diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 3870c67506b42..ba928931f303a 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 @@ -245,3 +246,9 @@ enabled: - x-pack/test/ui_capabilities/spaces_only/config.ts - x-pack/test/upgrade_assistant_integration/config.js - x-pack/test/usage_collection/config.ts + - x-pack/test/performance/journeys/ecommerce_dashboard/config.ts + - x-pack/test/performance/journeys/flight_dashboard/config.ts + - x-pack/test/performance/journeys/login/config.ts + - x-pack/test/performance/journeys/many_fields_discover/config.ts + - x-pack/test/performance/journeys/promotion_tracking_dashboard/config.ts + - x-pack/test/performance/journeys/web_logs_dashboard/config.ts diff --git a/.buildkite/pipelines/code_coverage/daily.yml b/.buildkite/pipelines/code_coverage/daily.yml index a2b501a39b088..c9bb675ab40cf 100644 --- a/.buildkite/pipelines/code_coverage/daily.yml +++ b/.buildkite/pipelines/code_coverage/daily.yml @@ -13,7 +13,8 @@ steps: queue: kibana-default env: FTR_CONFIGS_DEPS: '' - LIMIT_CONFIG_TYPE: 'unit,functional,integration' +# LIMIT_CONFIG_TYPE: 'unit,functional,integration' + LIMIT_CONFIG_TYPE: 'unit,integration' JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/code_coverage/jest.sh' JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/code_coverage/jest_integration.sh' FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/code_coverage/ftr_configs.sh' @@ -25,6 +26,6 @@ steps: depends_on: - jest - jest-integration - - ftr-configs +# - ftr-configs timeout_in_minutes: 30 key: ingest diff --git a/.buildkite/scripts/steps/code_coverage/ingest.sh b/.buildkite/scripts/steps/code_coverage/ingest.sh index a2f1b572252a5..a39097f706262 100755 --- a/.buildkite/scripts/steps/code_coverage/ingest.sh +++ b/.buildkite/scripts/steps/code_coverage/ingest.sh @@ -27,7 +27,7 @@ echo "--- Upload new git sha" echo "--- Download coverage artifacts" buildkite-agent artifact download target/kibana-coverage/jest/* . -buildkite-agent artifact download target/kibana-coverage/functional/* . +#buildkite-agent artifact download target/kibana-coverage/functional/* . buildkite-agent artifact download target/ran_files/* . ls -l target/ran_files/* || echo "### No ran-files found" @@ -42,20 +42,20 @@ echo "--- Jest: Reset file paths prefix, merge coverage files, and generate the replacePaths "$KIBANA_DIR/target/kibana-coverage/jest" "CC_REPLACEMENT_ANCHOR" "$KIBANA_DIR" yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.jest.config.js -echo "--- Functional: Reset file paths prefix, merge coverage files, and generate the final combined report" +#echo "--- Functional: Reset file paths prefix, merge coverage files, and generate the final combined report" # Functional: Reset file paths prefix to Kibana Dir of final worker -set +e -sed -ie "s|CC_REPLACEMENT_ANCHOR|${KIBANA_DIR}|g" target/kibana-coverage/functional/*.json -echo "--- Begin Split and Merge for Functional" -splitCoverage target/kibana-coverage/functional -splitMerge -set -e +#set +e +#sed -ie "s|CC_REPLACEMENT_ANCHOR|${KIBANA_DIR}|g" target/kibana-coverage/functional/*.json +#echo "--- Begin Split and Merge for Functional" +#splitCoverage target/kibana-coverage/functional +#splitMerge +#set -e echo "--- Archive and upload combined reports" collectAndUpload target/kibana-coverage/jest/kibana-jest-coverage.tar.gz \ target/kibana-coverage/jest-combined -collectAndUpload target/kibana-coverage/functional/kibana-functional-coverage.tar.gz \ - target/kibana-coverage/functional-combined +#collectAndUpload target/kibana-coverage/functional/kibana-functional-coverage.tar.gz \ +# target/kibana-coverage/functional-combined echo "--- Upload coverage static site" .buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh diff --git a/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh b/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh index c1df5ff4b39cf..de006352d0b09 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh @@ -38,14 +38,20 @@ echo "### Generate Team Assignments" CI_STATS_DISABLED=true node scripts/generate_team_assignments.js \ --verbose --src '.github/CODEOWNERS' --dest $TEAM_ASSIGN_PATH -for x in functional jest; do - echo "### Ingesting coverage for ${x}" - COVERAGE_SUMMARY_FILE="target/kibana-coverage/${x}-combined/coverage-summary.json" - - CI_STATS_DISABLED=true node scripts/ingest_coverage.js --path ${COVERAGE_SUMMARY_FILE} \ - --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH & -done -wait +#for x in functional jest; do +# echo "### Ingesting coverage for ${x}" +# COVERAGE_SUMMARY_FILE="target/kibana-coverage/${x}-combined/coverage-summary.json" +# +# CI_STATS_DISABLED=true node scripts/ingest_coverage.js --path ${COVERAGE_SUMMARY_FILE} \ +# --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH & +#done +#wait + +echo "### Ingesting coverage for JEST" +COVERAGE_SUMMARY_FILE="target/kibana-coverage/jest-combined/coverage-summary.json" + +CI_STATS_DISABLED=true node scripts/ingest_coverage.js --path ${COVERAGE_SUMMARY_FILE} \ + --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH echo "--- Ingesting Code Coverage - Complete" echo "" diff --git a/.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh b/.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh index f982ee5c581ce..0b6d0ce8ea105 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh @@ -4,7 +4,6 @@ set -euo pipefail cat << EOF > src/dev/code_coverage/www/index_partial_2.html Latest Jest - Latest FTR @@ -26,4 +25,4 @@ cat << EOF > src/dev/code_coverage/www/index_partial_2.html EOF cat src/dev/code_coverage/www/index_partial.html > src/dev/code_coverage/www/index.html -cat src/dev/code_coverage/www/index_partial_2.html >> src/dev/code_coverage/www/index.html \ No newline at end of file +cat src/dev/code_coverage/www/index_partial_2.html >> src/dev/code_coverage/www/index.html diff --git a/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh b/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh index 4d3fb92cf2b43..dcb0b03b16d7c 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh @@ -11,8 +11,10 @@ for x in 'src/dev/code_coverage/www/index.html' 'src/dev/code_coverage/www/404.h gsutil -m -q cp -r -a public-read -z js,css,html ${x} ${uploadPrefix} done -gsutil -m -q cp -r -a public-read -z js,css,html ${x} ${uploadPrefixWithTimeStamp} +#gsutil -m -q cp -r -a public-read -z js,css,html ${x} ${uploadPrefixWithTimeStamp} +# +#for x in 'target/kibana-coverage/functional-combined' 'target/kibana-coverage/jest-combined'; do +# gsutil -m -q cp -r -a public-read -z js,css,html ${x} ${uploadPrefixWithTimeStamp} +#done -for x in 'target/kibana-coverage/functional-combined' 'target/kibana-coverage/jest-combined'; do - gsutil -m -q cp -r -a public-read -z js,css,html ${x} ${uploadPrefixWithTimeStamp} -done +gsutil -m -q cp -r -a public-read -z js,css,html 'target/kibana-coverage/jest-combined' ${uploadPrefixWithTimeStamp} diff --git a/.buildkite/scripts/steps/functional/performance_playwright.sh b/.buildkite/scripts/steps/functional/performance_playwright.sh index a1c3f23ced51e..e4553fcf09ca1 100644 --- a/.buildkite/scripts/steps/functional/performance_playwright.sh +++ b/.buildkite/scripts/steps/functional/performance_playwright.sh @@ -24,24 +24,20 @@ for i in "${journeys[@]}"; do echo "JOURNEY[${i}] is running" export TEST_PERFORMANCE_PHASE=WARMUP - export ELASTIC_APM_ACTIVE=false export JOURNEY_NAME="${i}" checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: WARMUP)" \ node scripts/functional_tests \ - --config x-pack/test/performance/config.playwright.ts \ - --include "x-pack/test/performance/tests/playwright/${i}.ts" \ + --config "x-pack/test/performance/journeys/${i}/config.ts" \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ --debug \ --bail export TEST_PERFORMANCE_PHASE=TEST - export ELASTIC_APM_ACTIVE=true checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: TEST)" \ node scripts/functional_tests \ - --config x-pack/test/performance/config.playwright.ts \ - --include "x-pack/test/performance/tests/playwright/${i}.ts" \ + --config "x-pack/test/performance/journeys/${i}/config.ts" \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ --debug \ --bail diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index c541f59548753..ef790f79fff96 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -16,6 +16,7 @@ const STORYBOOKS = [ 'canvas', 'ci_composite', 'cloud', + 'coloring', 'controls', 'custom_integrations', 'dashboard_enhanced', diff --git a/.eslintrc.js b/.eslintrc.js index b4dbfbaf8600b..036b2123ee254 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -657,6 +657,7 @@ module.exports = { 'x-pack/test/*/*config.*ts', 'x-pack/test/saved_object_api_integration/*/apis/**/*', 'x-pack/test/ui_capabilities/*/tests/**/*', + 'x-pack/test/performance/**/*.ts', ], rules: { 'import/no-default-export': 'off', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 69245de6eb810..ceb9137f9f56e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -72,6 +72,7 @@ /src/plugins/navigation/ @elastic/kibana-app-services /src/plugins/share/ @elastic/kibana-app-services /src/plugins/ui_actions/ @elastic/kibana-app-services +/src/plugins/ui_actions_enhanced/ @elastic/kibana-app-services /src/plugins/data_view_field_editor @elastic/kibana-app-services /src/plugins/screenshot_mode @elastic/kibana-app-services /src/plugins/bfetch/ @elastic/kibana-app-services @@ -80,7 +81,6 @@ /src/plugins/unified_search/ @elastic/kibana-app-services /x-pack/examples/ui_actions_enhanced_examples/ @elastic/kibana-app-services /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-services -/x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-services /x-pack/plugins/runtime_fields @elastic/kibana-app-services /x-pack/test/search_sessions_integration/ @elastic/kibana-app-services /src/plugins/dashboard/public/application/embeddable/viewport/print_media @elastic/kibana-app-services @@ -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/.i18nrc.json b/.i18nrc.json index a5d1b36ede73d..0ab8e6b490a12 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -77,6 +77,7 @@ "src/plugins/vis_types/timelion" ], "uiActions": "src/plugins/ui_actions", + "uiActionsEnhanced": "src/plugins/ui_actions_enhanced", "uiActionsExamples": "examples/ui_action_examples", "usageCollection": "src/plugins/usage_collection", "utils": "packages/kbn-securitysolution-utils/src", diff --git a/dev_docs/contributing/how_we_use_github.mdx b/dev_docs/contributing/how_we_use_github.mdx index a45cad425a0e5..339eebc89197b 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,37 @@ 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 +- `triage-needed`: Indicates that someone from the area team needs to investigate. +- `needs_team`: Indicates that issue is missing area team label. All issues should be assigned to one or more area teams for follow up. This label is assigned automatically and removed automatically once a team label is added. + +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/getting_started/troubleshooting.mdx b/dev_docs/getting_started/troubleshooting.mdx index db52830bbae4f..b224a3200eefb 100644 --- a/dev_docs/getting_started/troubleshooting.mdx +++ b/dev_docs/getting_started/troubleshooting.mdx @@ -40,3 +40,12 @@ it means you are using a new Elasticsearch feature that will not work in a CCS e We added this test coverage in version `8.1` because we accidentally broke core Kibana features (for example, when Discover started using the new fields parameter) for our CCS users. CCS is not a corner case and (excluding certain experimental features) Kibana should always work for our CCS users. This setting is our way of ensuring test coverage. Please reach out to the [Kibana Operations team](https://github.com/orgs/elastic/teams/kibana-operations) if you have further questions. + +### Minified React errors + +If you experience minified React errors and want to expand them to their full error messages you will currently need to rebuild the `@kbn/ui-shared-deps-npm` package using "development" mode instead of "production", which can be done by modifying the corresponding line in `packages/kbn-ui-shared-deps-npm/webpack.config.js` and make sure to run `yarn kbn bootstrap` afterwards: + +```diff +- mode: 'production', ++ mode: 'development', +``` diff --git a/dev_docs/operations/operations_landing.mdx b/dev_docs/operations/operations_landing.mdx index 40c3ae3560768..95fb4ceb2af09 100644 --- a/dev_docs/operations/operations_landing.mdx +++ b/dev_docs/operations/operations_landing.mdx @@ -28,6 +28,7 @@ layout: landing { pageId: "kibDevDocsOpsBabelPluginSyntheticPackages" }, { pageId: "kibDevDocsOpsUiSharedDepsNpm" }, { pageId: "kibDevDocsOpsUiSharedDepsSrc" }, + { pageId: "kibDevDocsOpsPluginDiscovery" }, ]} /> @@ -50,5 +51,7 @@ layout: landing { pageId: "kibDevDocsOpsAmbientUiTypes" }, { pageId: "kibDevDocsOpsTestSubjSelector" }, { pageId: "kibDevDocsOpsBazelRunner" }, + { pageId: "kibDevDocsOpsCliDevMode" }, + { pageId: "kibDevDocsOpsEs" }, ]} /> \ No newline at end of file diff --git a/dev_docs/tutorials/advanced_settings.mdx b/dev_docs/tutorials/advanced_settings.mdx index 4e5e208fc68f0..1ca925e24f54a 100644 --- a/dev_docs/tutorials/advanced_settings.mdx +++ b/dev_docs/tutorials/advanced_settings.mdx @@ -286,3 +286,13 @@ export const migrations = { }; ``` [1] Since all `uiSettings` migrations are added to the same migration function, while not required, grouping settings by team is good practice. + +### Creating Transforms + +If you have need to make a change that isn't possible in a saved object migration function (for example, you need to find other saved +objects), you can create a transform function instead. This will be applied when a `config` saved object is first created, and/or when it is +first upgraded. Note that you might need to add an extra attribute to verify that this transform has already been applied so it doesn't get +applied again in the future. + +For example, we needed to transform the `defaultIndex` attribute, and we added an extra `isDefaultIndexMigrated` attribute for this purpose. +See `src/core/server/ui_settings/saved_objects/transforms.ts` and [#13339](https://github.com/elastic/kibana/pull/133339) for an example. diff --git a/docs/api/actions-and-connectors.asciidoc b/docs/api/actions-and-connectors.asciidoc index f8d286e00b856..b35fa57668859 100644 --- a/docs/api/actions-and-connectors.asciidoc +++ b/docs/api/actions-and-connectors.asciidoc @@ -28,7 +28,7 @@ include::actions-and-connectors/delete.asciidoc[leveloffset=+1] include::actions-and-connectors/get.asciidoc[leveloffset=+1] include::actions-and-connectors/get_all.asciidoc[leveloffset=+1] include::actions-and-connectors/list.asciidoc[] -include::actions-and-connectors/update.asciidoc[] +include::actions-and-connectors/update.asciidoc[leveloffset=+1] include::actions-and-connectors/execute.asciidoc[] include::actions-and-connectors/legacy/index.asciidoc[] include::actions-and-connectors/legacy/get.asciidoc[] diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc index d5208b9debfe9..b8334573d2330 100644 --- a/docs/api/actions-and-connectors/create.asciidoc +++ b/docs/api/actions-and-connectors/create.asciidoc @@ -6,7 +6,6 @@ Creates a connector. -[discrete] [[create-connector-api-request]] === {api-request-title} @@ -14,14 +13,12 @@ Creates a connector. `POST :/s//api/actions/connector` -[discrete] === {api-prereq-title} You must have `all` privileges for the *Actions and Connectors* feature in the *Management* section of the <>. -[discrete] [[create-connector-api-path-params]] === {api-path-parms-title} @@ -29,34 +26,88 @@ You must have `all` privileges for the *Actions and Connectors* feature in the (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. -[discrete] +[role="child_attributes"] [[create-connector-api-request-body]] === {api-request-body-title} -`name`:: - (Required, string) The display name for the connector. +`config`:: +(Required, object) The configuration for the connector. Configuration properties +vary depending on the connector type. For example: ++ +-- +// tag::connector-config[] +.Index connectors +[%collapsible%open] +==== +`executionTimeField`:: +(Optional, string) Specifies a field that will contain the time the alert +condition was detected. The default value is `null`. + +`index`:: +(Required, string) The {es} index to be written to. + +`refresh`:: +(Optional, boolean) The {ref}/docs-refresh.html[refresh] policy for the write +request. The default value is `false`. + +For more information, refer to +{kibana-ref}/index-action-type.html[Index connector and action]. +==== + +.{jira} connectors +[%collapsible%open] +==== + +`apiUrl`:: +(Required, string) The {jira} instance URL. + +`projectKey`:: +(Required, string) The {jira} project key. + +For more information, refer to +{kibana-ref}/jira-action-type.html[{jira} connector and action]. +==== + +For more configuration properties, refer to <>. +// end::connector-config[] +-- `connector_type_id`:: - (Required, string) The connector type ID for the connector. +(Required, string) The connector type ID for the connector. -`config`:: - (Required, object) The configuration for the connector. Configuration properties vary depending on - the connector type. For information about the configuration properties, refer to <>. +`name`:: +(Required, string) The display name for the connector. `secrets`:: - (Required, object) The secrets configuration for the connector. Secrets configuration properties vary - depending on the connector type. For information about the secrets configuration properties, refer to <>. +(Required^*^, object) The secrets configuration for the connector. Secrets +configuration properties vary depending on the connector type. For information +about the secrets configuration properties, refer to <>. + +-- WARNING: Remember these values. You must provide them each time you call the <> API. +-- ++ +-- +// tag::connector-secrets[] +.{jira} connectors +[%collapsible%open] +==== +`apiToken`:: +(Required, string) The {jira} API authentication token for HTTP basic +authentication. + +`email`:: +(Required, string) The account email for HTTP Basic authentication. +==== +// end::connector-secrets[] +-- -[discrete] [[create-connector-api-request-codes]] === {api-response-codes-title} `200`:: Indicates a successful call. -[discrete] [[create-connector-api-example]] === {api-examples-title} diff --git a/docs/api/actions-and-connectors/execute.asciidoc b/docs/api/actions-and-connectors/execute.asciidoc index e830c9b4bbf88..2ddd311836281 100644 --- a/docs/api/actions-and-connectors/execute.asciidoc +++ b/docs/api/actions-and-connectors/execute.asciidoc @@ -7,14 +7,24 @@ Executes a connector by ID. [[execute-connector-api-request]] -==== Request +==== {api-request-title} `POST :/api/actions/connector//_execute` `POST :/s//api/actions/connector//_execute` +[discrete] +=== {api-prereq-title} + +You must have `read` privileges for the *Actions and Connectors* feature in the +*Management* section of the +<>. + +If you use an index connector, you must also have `all`, `create`, `index`, or +`write` {ref}/security-privileges.html[indices privileges]. + [[execute-connector-api-params]] -==== Path parameters +==== {api-path-parms-title} `id`:: (Required, string) The ID of the connector. @@ -23,24 +33,26 @@ Executes a connector by ID. (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. [[execute-connector-api-request-body]] -==== Request body +==== {api-request-body-title} `params`:: (Required, object) The parameters of the connector. Parameter properties vary depending on the connector type. For information about the parameter properties, refer to <>. [[execute-connector-api-codes]] -==== Response code +==== {api-response-codes-title} `200`:: Indicates a successful call. [[execute-connector-api-example]] -==== Example +==== {api-examples-title} + +Run an index connector: [source,sh] -------------------------------------------------- -$ curl -X POST api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad/_execute -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +POST api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad/_execute { "params": { "documents": [ @@ -51,7 +63,7 @@ $ curl -X POST api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad/_execu } ] } -}' +} -------------------------------------------------- // KIBANA @@ -62,7 +74,7 @@ The API returns the following: { "status": "ok", "data": { - "took": 197, + "took": 10, "errors": false, "items": [ { @@ -76,7 +88,7 @@ The API returns the following: "successful": 1, "failed": 0 }, - "_seq_no": 0, + "_seq_no": 7, "_primary_term": 1, "status": 201 } diff --git a/docs/api/actions-and-connectors/list.asciidoc b/docs/api/actions-and-connectors/list.asciidoc index bd1ccb777b9ae..a2542c6ca3aaf 100644 --- a/docs/api/actions-and-connectors/list.asciidoc +++ b/docs/api/actions-and-connectors/list.asciidoc @@ -7,30 +7,37 @@ Retrieves a list of all connector types. [[list-connector-types-api-request]] -==== Request +==== {api-request-title} `GET :/api/actions/connector_types` `GET :/s//api/actions/connector_types` + +[discrete] +=== {api-prereq-title} + +You do not need any <> to +run this API. + [[list-connector-types-api-path-params]] -==== Path parameters +==== {api-path-parms-title} `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. [[list-connector-types-api-codes]] -==== Response code +==== {api-response-codes-title} `200`:: Indicates a successful call. [[list-connector-types-api-example]] -==== Example +==== {api-examples-title} [source,sh] -------------------------------------------------- -$ curl -X GET api/actions/connector_types +GET api/actions/connector_types -------------------------------------------------- // KIBANA @@ -45,7 +52,7 @@ The API returns the following: "minimum_license_required": "gold", <3> "enabled": false, <4> "enabled_in_config": true, <5> - "enabled_in_license": false <6> + "enabled_in_license": true <6> }, { "id": ".index", @@ -54,14 +61,13 @@ The API returns the following: "enabled": true, "enabled_in_config": true, "enabled_in_license": true - } + }, + ... ] -------------------------------------------------- - - <1> `id` - The unique ID of the connector type. <2> `name` - The name of the connector type. <3> `minimum_license_required` - The license required to use the connector type. <4> `enabled` - Specifies if the connector type is enabled or disabled in {kib}. -<5> `enabled_in_config` - Specifies if the connector type is enabled or enabled in the {kib} .yml file. +<5> `enabled_in_config` - Specifies if the connector type is enabled or enabled in the {kib} `.yml` file. <6> `enabled_in_license` - Specifies if the connector type is enabled or disabled in the license. diff --git a/docs/api/actions-and-connectors/update.asciidoc b/docs/api/actions-and-connectors/update.asciidoc index 7ccb10714f474..58056cb8085f9 100644 --- a/docs/api/actions-and-connectors/update.asciidoc +++ b/docs/api/actions-and-connectors/update.asciidoc @@ -1,20 +1,27 @@ [[update-connector-api]] -=== Update connector API +== Update connector API ++++ Update connector ++++ -Updates the attributes for an existing connector. +Updates the attributes for a connector. [[update-connector-api-request]] -==== Request +=== {api-request-title} `PUT :/api/actions/connector/` `PUT :/s//api/actions/connector/` +[discrete] +=== {api-prereq-title} + +You must have `all` privileges for the *Actions and Connectors* feature in the +*Management* section of the +<>. + [[update-connector-api-params]] -==== Path parameters +=== {api-path-parms-title} `id`:: (Required, string) The ID of the connector. @@ -22,36 +29,49 @@ Updates the attributes for an existing connector. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +[role="child_attributes"] [[update-connector-api-request-body]] -==== Request body - -`name`:: - (Required, string) The new name of the connector. +=== {api-request-body-title} `config`:: - (Required, object) The new connector configuration. Configuration properties vary depending on the connector type. For information about the configuration properties, refer to <>. +(Required, object) The new connector configuration. Configuration properties +vary depending on the connector type. For example: ++ +-- +include::create.asciidoc[tag=connector-config] +-- + +`name`:: +(Required, string) The new name of the connector. `secrets`:: - (Required, object) The updated secrets configuration for the connector. Secrets properties vary depending on the connector type. For information about the secrets configuration properties, refer to <>. +(Required^*^, object) The updated secrets configuration for the connector. Secrets +properties vary depending on the connector type. For information about the +secrets configuration properties, refer to +<>. ++ +-- +include::create.asciidoc[tag=connector-secrets] +-- [[update-connector-api-codes]] -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. [[update-connector-api-example]] -==== Example +=== {api-examples-title} [source,sh] -------------------------------------------------- -$ curl -X PUT api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +PUT api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad { "name": "updated-connector", "config": { "index": "updated-index" } -}' +} -------------------------------------------------- // KIBANA diff --git a/docs/api/cases/cases-api-create.asciidoc b/docs/api/cases/cases-api-create.asciidoc index b39125cf7538e..402521a4c31e7 100644 --- a/docs/api/cases/cases-api-create.asciidoc +++ b/docs/api/cases/cases-api-create.asciidoc @@ -162,13 +162,9 @@ categorize cases. It can be an empty array. -------------------------------------------------- POST api/cases { - "description": "James Bond clicked on a highly suspicious email - banner advertising cheap holidays for underpaid civil servants.", - "title": "This case will self-destruct in 5 seconds", - "tags": [ - "phishing", - "social engineering" - ], + "description": "A case description.", + "title": "Case title 1", + "tags": [ "tag 1" ], "connector": { "id": "131d4448-abe0-4789-939d-8ef60680b498", "name": "My connector", @@ -182,7 +178,7 @@ POST api/cases "settings": { "syncAlerts": true }, - "owner": "securitySolution" + "owner": "cases" } -------------------------------------------------- // KIBANA @@ -198,26 +194,22 @@ the case identifier, version, and creation time. For example: "comments": [], "totalComment": 0, "totalAlerts": 0, - "title": "This case will self-destruct in 5 seconds", - "tags": [ - "phishing", - "social engineering", - "bubblegum" - ], + "title": "Case title 1", + "tags": [ "tag 1" ], "settings": { "syncAlerts": true }, - "owner": "securitySolution", - "description": "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", + "owner": "cases", + "description": "A case description.", "duration": null, "severity": "low", "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", "created_by": { - "email": "ahunley@imf.usa.gov", - "full_name": "Alan Hunley", - "username": "ahunley" + "email": null, + "full_name": null, + "username": "elastic" }, "status": "open", "updated_at": null, diff --git a/docs/api/cases/cases-api-find-cases.asciidoc b/docs/api/cases/cases-api-find-cases.asciidoc index c643bc64cb982..d7ae5fed35b0f 100644 --- a/docs/api/cases/cases-api-find-cases.asciidoc +++ b/docs/api/cases/cases-api-find-cases.asciidoc @@ -95,12 +95,12 @@ Defaults to `desc`. === {api-examples-title} -Retrieve the first five cases with the `phishing` tag, in ascending order by +Retrieve the first five cases with the `tag-1` tag, in ascending order by last update time: [source,sh] -------------------------------------------------- -GET api/cases/_find?page=1&perPage=5&sortField=updatedAt&sortOrder=asc&tags=phishing +GET api/cases/_find?page=1&perPage=5&sortField=updatedAt&sortOrder=asc&tags=tag-1 -------------------------------------------------- // KIBANA @@ -120,11 +120,11 @@ The API returns a JSON object listing the retrieved cases. For example: "totalComment": 1, "totalAlerts": 0, "title": "Case title", - "tags": [ "phishing" ], + "tags": [ "tag-1" ], "description": "Case description", "settings": { "syncAlerts": true }, "owner": "securitySolution", - "duration": null, + "duration": null, <1> "severity": "low", "closed_at": null, "closed_by": null, @@ -155,3 +155,7 @@ The API returns a JSON object listing the retrieved cases. For example: "count_closed_cases": 0 } -------------------------------------------------- +<1> Duration represents 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`. If the case was closed after less than half a second, the duration is +rounded down to zero. \ No newline at end of file diff --git a/docs/api/cases/cases-api-get-case.asciidoc b/docs/api/cases/cases-api-get-case.asciidoc index a3adc90fe09bf..70cc7f5bab60c 100644 --- a/docs/api/cases/cases-api-get-case.asciidoc +++ b/docs/api/cases/cases-api-get-case.asciidoc @@ -92,11 +92,15 @@ The API returns a JSON object with the retrieved case. For example: }, "owner": "securitySolution", "severity": "low", - "duration": null, + "duration": null, <1> "tags": [ "phishing", "social engineering", "bubblegum" ] } --------------------------------------------------- \ No newline at end of file +-------------------------------------------------- +<1> Duration represents 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`. If the case was closed after less than half a second, the duration is +rounded down to zero. \ 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 3bd2e8debb3cd..01aec7a7e4c77 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 778e95949e3f5..46cb11dc63b03 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 6a7a7c26c66d2..89ec6f0600717 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 cf7d2ea7d8cfd..c1dcb2a71e57c 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/cases/cases-api-update.asciidoc b/docs/api/cases/cases-api-update.asciidoc index ea33394a6ee63..687622cce8ddd 100644 --- a/docs/api/cases/cases-api-update.asciidoc +++ b/docs/api/cases/cases-api-update.asciidoc @@ -159,7 +159,8 @@ and `open`. (Optional, string) A title for the case. `version`:: -(Required, string) The current version of the case. To determine this value, use <> or <>. +(Required, string) The current version of the case. To determine this value, use +<> or <>. ==== === {api-response-codes-title} @@ -190,15 +191,8 @@ PATCH api/cases "parent": null } }, - "description": "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!", - "tags": [ - "phishing", - "social engineering", - "bubblegum" - ], + "description": "A new description.", + "tags": [ "tag-1", "tag-2" ], "settings": { "syncAlerts": true } @@ -219,33 +213,29 @@ The API returns the updated case with a new `version` value. For example: "comments": [], "totalComment": 0, "totalAlerts": 0, - "title": "This case will self-destruct in 5 seconds", - "tags": [ - "phishing", - "social engineering", - "bubblegum" - ], + "title": "Case title 1", + "tags": [ "tag-1", "tag-2" ], "settings": { "syncAlerts": true }, - "owner": "securitySolution", - "description": "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!", + "owner": "cases", + "description": "A new description.", "duration": null, "severity": "low", "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", "created_by": { - "email": "ahunley@imf.usa.gov", - "full_name": "Alan Hunley", - "username": "ahunley" + "email": null, + "full_name": null, + "username": "elastic" }, "status": "open", "updated_at": "2022-05-13T09:48:33.043Z", "updated_by": { - "email": "classified@hms.oo.gov.uk", - "full_name": "Classified", - "username": "M" + "email": null, + "full_name": null, + "username": "elastic" }, "connector": { "id": "131d4448-abe0-4789-939d-8ef60680b498", @@ -260,9 +250,9 @@ The API returns the updated case with a new `version` value. For example: "external_service": { "external_title": "IS-4", "pushed_by": { - "full_name": "Classified", - "email": "classified@hms.oo.gov.uk", - "username": "M" + "full_name": null, + "email": null, + "username": "elastic" }, "external_url": "https://hms.atlassian.net/browse/IS-4", "pushed_at": "2022-05-13T09:20:40.672Z", diff --git a/docs/api/data-views.asciidoc b/docs/api/data-views.asciidoc index d7380cbd97c99..cf9524d4fdf30 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 0000000000000..42727c38f6d98 --- /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 0000000000000..c43a512768fad --- /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 b4e49a69d5a7e..b634c937588b0 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 0000000000000..7ff5cb5e9d0ba 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 0000000000000..ebd992abe9303 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 2201ed5fcaa72..2c8ebace287b8 100644 Binary files a/docs/apm/images/apm-settings.png and b/docs/apm/images/apm-settings.png differ diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 837e8845ec542..919727dfe8625 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -291,6 +291,10 @@ In general this plugin provides: - Exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger. +|{kib-repo}blob/{branch}/src/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] +|Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. + + |{kib-repo}blob/{branch}/src/plugins/unified_search/README.md[unifiedSearch] |Contains all the components of Kibana's unified search experience. Specifically: @@ -649,10 +653,6 @@ For adding localizations and instrument a ui to support translated content, see As a developer you can reuse and extend built-in alerts and actions UI functionality: -|{kib-repo}blob/{branch}/x-pack/plugins/ui_actions_enhanced/README.md[uiActionsEnhanced] -|Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. - - |{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant/README.md[upgradeAssistant] |Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. It will only be enabled on the last minor before the next major release. This is controlled via the config: xpack.upgrade_assistant.readonly (#101296). diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpmenuactions.hidehelpmenu.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpmenuactions.hidehelpmenu.md index 9bd8b107b2a0a..bcd67a8fe6f21 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpmenuactions.hidehelpmenu.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpmenuactions.hidehelpmenu.md @@ -4,8 +4,6 @@ ## ChromeHelpMenuActions.hideHelpMenu property -The action provides the capability to hide the help menu from within the help extension content components. - Signature: ```typescript diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpmenuactions.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpmenuactions.md index 3cf33deb3db15..f33581cda5879 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpmenuactions.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpmenuactions.md @@ -4,6 +4,7 @@ ## ChromeHelpMenuActions interface + Signature: ```typescript @@ -15,3 +16,4 @@ export interface ChromeHelpMenuActions | Property | Type | Description | | --- | --- | --- | | [hideHelpMenu](./kibana-plugin-core-public.chromehelpmenuactions.hidehelpmenu.md) | () => void | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.injectedmetadata.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.injectedmetadata.md index b416670a17210..661702f2d466e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.injectedmetadata.md +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.injectedmetadata.md @@ -4,17 +4,9 @@ ## CoreSetup.injectedMetadata property -> Warning: This API is now obsolete. -> -> 8.8.0 -> - -exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Signature: ```typescript -injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; +injectedMetadata: InjectedMetadataSetup; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.md index 0298ac904f952..d83fe780a5a6f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.md @@ -22,7 +22,7 @@ export interface CoreSetup Warning: This API is now obsolete. -> -> 8.8.0 -> - -exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Signature: ```typescript -injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; +injectedMetadata: InjectedMetadataStart; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md index 34576c4df2e40..19d4c7115417e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -25,7 +25,7 @@ export interface CoreStart | [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | | [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-core-public.httpstart.md) | | [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | -| [injectedMetadata](./kibana-plugin-core-public.corestart.injectedmetadata.md) | { getInjectedVar: (name: string, defaultValue?: any) => unknown; } | exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. | +| [injectedMetadata](./kibana-plugin-core-public.corestart.injectedmetadata.md) | InjectedMetadataStart | | | [notifications](./kibana-plugin-core-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | | [overlays](./kibana-plugin-core-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-core-public.overlaystart.md) | | [savedObjects](./kibana-plugin-core-public.corestart.savedobjects.md) | SavedObjectsStart | [SavedObjectsStart](./kibana-plugin-core-public.savedobjectsstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 4f119d543543e..4c5cd8378af60 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -46,7 +46,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md) | | | [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md) | | | [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-core-public.chromehelpextensionmenugithublink.md) | | -| [ChromeHelpMenuActions](./kibana-plugin-core-public.chromehelpmenuactions.md) | List of actions in order to manipulate with the help menu from the help extensions content components. | +| [ChromeHelpMenuActions](./kibana-plugin-core-public.chromehelpmenuactions.md) | | | [ChromeNavControl](./kibana-plugin-core-public.chromenavcontrol.md) | | | [ChromeNavControls](./kibana-plugin-core-public.chromenavcontrols.md) | [APIs](./kibana-plugin-core-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | | [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.configpath.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.configpath.md deleted file mode 100644 index f58c717c80395..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.configpath.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [configPath](./kibana-plugin-core-server.discoveredplugin.configpath.md) - -## DiscoveredPlugin.configPath property - -Root configuration path used by the plugin, defaults to "id" in snake\_case format. - -Signature: - -```typescript -readonly configPath: ConfigPath; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.enabledonanonymouspages.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.enabledonanonymouspages.md deleted file mode 100644 index 472bac3dde7d8..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.enabledonanonymouspages.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [enabledOnAnonymousPages](./kibana-plugin-core-server.discoveredplugin.enabledonanonymouspages.md) - -## DiscoveredPlugin.enabledOnAnonymousPages property - -Specifies whether this plugin - and its required dependencies - will be enabled for anonymous pages (login page, status page when configured, etc.) Default is false. - -Signature: - -```typescript -readonly enabledOnAnonymousPages?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.id.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.id.md deleted file mode 100644 index 0a2d091a31fba..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.id.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [id](./kibana-plugin-core-server.discoveredplugin.id.md) - -## DiscoveredPlugin.id property - -Identifier of the plugin. - -Signature: - -```typescript -readonly id: PluginName; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md deleted file mode 100644 index 258acfa9ddc36..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) - -## DiscoveredPlugin interface - -Small container object used to expose information about discovered plugins that may or may not have been started. - -Signature: - -```typescript -export interface DiscoveredPlugin -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [configPath](./kibana-plugin-core-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id" in snake\_case format. | -| [enabledOnAnonymousPages?](./kibana-plugin-core-server.discoveredplugin.enabledonanonymouspages.md) | boolean | (Optional) Specifies whether this plugin - and its required dependencies - will be enabled for anonymous pages (login page, status page when configured, etc.) Default is false. | -| [id](./kibana-plugin-core-server.discoveredplugin.id.md) | PluginName | Identifier of the plugin. | -| [optionalPlugins](./kibana-plugin-core-server.discoveredplugin.optionalplugins.md) | readonly PluginName\[\] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | -| [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md) | readonly PluginName\[\] | List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins. | -| [requiredPlugins](./kibana-plugin-core-server.discoveredplugin.requiredplugins.md) | readonly PluginName\[\] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | -| [type](./kibana-plugin-core-server.discoveredplugin.type.md) | PluginType | Type of the plugin, defaults to standard. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.optionalplugins.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.optionalplugins.md deleted file mode 100644 index 0fc42048be90c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.optionalplugins.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [optionalPlugins](./kibana-plugin-core-server.discoveredplugin.optionalplugins.md) - -## DiscoveredPlugin.optionalPlugins property - -An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. - -Signature: - -```typescript -readonly optionalPlugins: readonly PluginName[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md deleted file mode 100644 index 6d54adb5236ea..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md) - -## DiscoveredPlugin.requiredBundles property - -List of plugin ids that this plugin's UI code imports modules from that are not in `requiredPlugins`. - -Signature: - -```typescript -readonly requiredBundles: readonly PluginName[]; -``` - -## Remarks - -The plugins listed here will be loaded in the browser, even if the plugin is disabled. Required by `@kbn/optimizer` to support cross-plugin imports. "core" and plugins already listed in `requiredPlugins` do not need to be duplicated here. - diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredplugins.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredplugins.md deleted file mode 100644 index b039891904669..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredplugins.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [requiredPlugins](./kibana-plugin-core-server.discoveredplugin.requiredplugins.md) - -## DiscoveredPlugin.requiredPlugins property - -An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. - -Signature: - -```typescript -readonly requiredPlugins: readonly PluginName[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.type.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.type.md deleted file mode 100644 index 0a33be0d63f5c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.type.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [type](./kibana-plugin-core-server.discoveredplugin.type.md) - -## DiscoveredPlugin.type property - -Type of the plugin, defaults to `standard`. - -Signature: - -```typescript -readonly type: PluginType; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md index be28cd76b16ee..e02f208ae86ac 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -10,7 +10,7 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom ```typescript kibanaResponseFactory: { - custom: | Error | Buffer | { + custom: | Buffer | Error | { message: string | Error; attributes?: ResponseErrorAttributes | undefined; } | Stream | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; @@ -34,7 +34,7 @@ kibanaResponseFactory: { message: string | Error; attributes?: ResponseErrorAttributes | undefined; }>; - customError: (options: CustomHttpResponseOptions) => KibanaResponse) => KibanaResponse; diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 124f744b08032..7a923aca3a771 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -37,7 +37,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | --- | --- | | [AuthResultType](./kibana-plugin-core-server.authresulttype.md) | | | [AuthStatus](./kibana-plugin-core-server.authstatus.md) | Status indicating an outcome of the authentication. | -| [PluginType](./kibana-plugin-core-server.plugintype.md) | | ## Interfaces @@ -68,7 +67,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DeprecationsClient](./kibana-plugin-core-server.deprecationsclient.md) | Server-side client that provides access to fetch all Kibana deprecations | | [DeprecationSettings](./kibana-plugin-core-server.deprecationsettings.md) | UiSettings deprecation field options. | | [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) | The deprecations service provides a way for the Kibana platform to communicate deprecated features and configs with its users. These deprecations are only communicated if the deployment is using these features. Allowing for a user tailored experience for upgrading the stack version.The Deprecation service is consumed by the upgrade assistant to assist with the upgrade experience.If a deprecated feature can be resolved without manual user intervention. Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the deprecation upon a user trigger. | -| [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | | [DocLinksServiceSetup](./kibana-plugin-core-server.doclinksservicesetup.md) | | | [ElasticsearchConfigPreboot](./kibana-plugin-core-server.elasticsearchconfigpreboot.md) | A limited set of Elasticsearch configuration entries exposed to the preboot plugins at setup. | | [ElasticsearchErrorDetails](./kibana-plugin-core-server.elasticsearcherrordetails.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugintype.md b/docs/development/core/server/kibana-plugin-core-server.plugintype.md deleted file mode 100644 index e4a252a392949..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.plugintype.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginType](./kibana-plugin-core-server.plugintype.md) - -## PluginType enum - - -Signature: - -```typescript -export declare enum PluginType -``` - -## Enumeration Members - -| Member | Value | Description | -| --- | --- | --- | -| preboot | "preboot" | Preboot plugins are special-purpose plugins that only function during preboot stage. | -| standard | "standard" | Standard plugins are plugins that start to function as soon as Kibana is fully booted and are active until it shuts down. | - diff --git a/docs/management/cases/images/cases-visualization.png b/docs/management/cases/images/cases-visualization.png index 77f249f26d091..53c4893bcc598 100644 Binary files a/docs/management/cases/images/cases-visualization.png and b/docs/management/cases/images/cases-visualization.png differ diff --git a/docs/management/cases/images/cases.png b/docs/management/cases/images/cases.png index b244b3df16a20..a69f21c2ec9f8 100644 Binary files a/docs/management/cases/images/cases.png and b/docs/management/cases/images/cases.png differ diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 2cfd3169b45a3..65f291a1c11cb 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 876f135da9356..f3adae184348f 100644 Binary files a/docs/settings/images/apm-settings.png and b/docs/settings/images/apm-settings.png differ diff --git a/docs/setup/upgrade/resolving-migration-failures.asciidoc b/docs/setup/upgrade/resolving-migration-failures.asciidoc index 2c3f66f2354dd..10aabdcabd5e2 100644 --- a/docs/setup/upgrade/resolving-migration-failures.asciidoc +++ b/docs/setup/upgrade/resolving-migration-failures.asciidoc @@ -89,9 +89,8 @@ The dashboard with the `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` ID that belongs to [[unknown-saved-object-types]] ==== Documents for unknown saved objects Migrations will fail if saved objects belong to an unknown -saved object type. Unknown saved objects are typically caused by -to the {es} index, or by disabling a plugin that had previously -created a saved object. +saved object type. Unknown saved objects are typically caused by performing manual modifications +to the {es} index (no longer allowed in 8.x), or by disabling a plugin that had previously created a saved object. We recommend using the {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant] to discover and remedy any unknown saved object types. {kib} version 7.17.0 deployments containing unknown saved @@ -106,7 +105,20 @@ If you fail to remedy this, your upgrade to 8.0+ will fail with a message like: [source,sh] -------------------------------------------- -Unable to complete saved object migrations for the [.kibana] index: Migration failed because documents were found for unknown saved object types. To proceed with the migration, please delete these documents from the ".kibana_7.17.0_001" index. +Unable to complete saved object migrations for the [.kibana] index: Migration failed because some documents were found which use unknown saved object types: +- "firstDocId" (type "someType") +- "secondtDocId" (type "someType") +- "thirdDocId" (type "someOtherType") + +To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration. +-------------------------------------------- + +To proceed with the migration, re-enable any plugins that previously created these saved objects. Alternatively, carefully review the list of unknown saved objects in the Kibana log entry. If the corresponding disabled plugins and their associated saved objects will no longer be used, they can be deleted by setting the configuration option `migrations.discardUnknownObjects` to the version you are upgrading to. +For instance, for an upgrade to 8.3.0, you can define the following setting in kibana.yml: + +[source,yaml] +-------------------------------------------- +migrations.discardUnknownObjects: "8.3.0" -------------------------------------------- [float] @@ -181,7 +193,7 @@ PUT /_cluster/settings { "transient": { "cluster.routing.allocation.enable": null - }, + }, "persistent": { "cluster.routing.allocation.enable": null } @@ -193,4 +205,4 @@ PUT /_cluster/settings ==== {es} cluster shard limit exceeded When upgrading, {kib} creates new indices requiring a small number of new shards. If the amount of open {es} shards approaches or exceeds the {es} `cluster.max_shards_per_node` setting, {kib} is unable to complete the upgrade. Ensure that {kib} is able to add at least 10 more shards by removing indices to clear up resources, or by increasing the `cluster.max_shards_per_node` setting. -For more information, refer to the documentation on {ref}/allocation-total-shards.html[total shards per node]. \ No newline at end of file +For more information, refer to the documentation on {ref}/allocation-total-shards.html[total shards per node]. diff --git a/fleet_packages.json b/fleet_packages.json index 5c62d96953a1a..9fefb0aa9a2c2 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -15,11 +15,11 @@ [ { "name": "apm", - "version": "8.2.0" + "version": "8.3.0" }, { "name": "elastic_agent", - "version": "1.3.1" + "version": "1.3.3" }, { "name": "endpoint", @@ -27,7 +27,7 @@ }, { "name": "fleet_server", - "version": "1.1.1" + "version": "1.2.0" }, { "name": "synthetics", diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 9a170aac79e58..0f2a349e60f01 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -184,7 +184,8 @@ { "id": "kibDevDocsOpsTypeSummarizer" }, { "id": "kibDevDocsOpsBabelPluginSyntheticPackages" }, { "id": "kibDevDocsOpsUiSharedDepsNpm" }, - { "id": "kibDevDocsOpsUiSharedDepsSrc" } + { "id": "kibDevDocsOpsUiSharedDepsSrc" }, + { "id": "kibDevDocsOpsPluginDiscovery" } ] }, { @@ -205,7 +206,9 @@ { "id": "kibDevDocsOpsAmbientStorybookTypes" }, { "id": "kibDevDocsOpsAmbientUiTypes" }, { "id": "kibDevDocsOpsTestSubjSelector" }, - { "id": "kibDevDocsOpsBazelRunner" } + { "id": "kibDevDocsOpsBazelRunner" }, + { "id": "kibDevDocsOpsCliDevMode" }, + { "id": "kibDevDocsOpsEs" } ] } ] diff --git a/package.json b/package.json index 6b8b832fd91a9..25f8918cb11c9 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "@elastic/charts": "46.0.1", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.2", - "@elastic/ems-client": "8.3.2", + "@elastic/ems-client": "8.3.3", "@elastic/eui": "58.0.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", @@ -151,6 +151,10 @@ "@kbn/core-base-common": "link:bazel-bin/packages/core/base/core-base-common", "@kbn/core-base-common-internal": "link:bazel-bin/packages/core/base/core-base-common-internal", "@kbn/core-base-server-internal": "link:bazel-bin/packages/core/base/core-base-server-internal", + "@kbn/core-injected-metadata-browser": "link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser", + "@kbn/core-injected-metadata-browser-internal": "link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser-internal", + "@kbn/core-injected-metadata-browser-mocks": "link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser-mocks", + "@kbn/core-injected-metadata-common-internal": "link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-common-internal", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto", "@kbn/datemath": "link:bazel-bin/packages/kbn-datemath", "@kbn/doc-links": "link:bazel-bin/packages/kbn-doc-links", @@ -205,6 +209,7 @@ "@kbn/ui-shared-deps-src": "link:bazel-bin/packages/kbn-ui-shared-deps-src", "@kbn/ui-theme": "link:bazel-bin/packages/kbn-ui-theme", "@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types", + "@kbn/utility-types-jest": "link:bazel-bin/packages/kbn-utility-types-jest", "@kbn/utils": "link:bazel-bin/packages/kbn-utils", "@loaders.gl/core": "^2.3.1", "@loaders.gl/json": "^2.3.1", @@ -647,6 +652,10 @@ "@types/kbn__core-base-server": "link:bazel-bin/packages/core/base/core-base-server/npm_module_types", "@types/kbn__core-base-server-internal": "link:bazel-bin/packages/core/base/core-base-server-internal/npm_module_types", "@types/kbn__core-common-internal-base": "link:bazel-bin/packages/core/common/internal-base/npm_module_types", + "@types/kbn__core-injected-metadata-browser": "link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser/npm_module_types", + "@types/kbn__core-injected-metadata-browser-internal": "link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser-internal/npm_module_types", + "@types/kbn__core-injected-metadata-browser-mocks": "link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser-mocks/npm_module_types", + "@types/kbn__core-injected-metadata-common-internal": "link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-common-internal/npm_module_types", "@types/kbn__core-public-internal-base": "link:bazel-bin/packages/core/public/internal-base/npm_module_types", "@types/kbn__core-server-internal-base": "link:bazel-bin/packages/core/server/internal-base/npm_module_types", "@types/kbn__crypto": "link:bazel-bin/packages/kbn-crypto/npm_module_types", @@ -720,6 +729,7 @@ "@types/kbn__ui-shared-deps-src": "link:bazel-bin/packages/kbn-ui-shared-deps-src/npm_module_types", "@types/kbn__ui-theme": "link:bazel-bin/packages/kbn-ui-theme/npm_module_types", "@types/kbn__utility-types": "link:bazel-bin/packages/kbn-utility-types/npm_module_types", + "@types/kbn__utility-types-jest": "link:bazel-bin/packages/kbn-utility-types-jest/npm_module_types", "@types/kbn__utils": "link:bazel-bin/packages/kbn-utils/npm_module_types", "@types/license-checker": "15.0.0", "@types/listr": "^0.14.0", @@ -777,7 +787,7 @@ "@types/redux-logger": "^3.0.8", "@types/resolve": "^1.20.1", "@types/seedrandom": ">=2.0.0 <4.0.0", - "@types/selenium-webdriver": "^4.1.0", + "@types/selenium-webdriver": "^4.1.1", "@types/semver": "^7", "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", @@ -953,7 +963,7 @@ "resolve": "^1.22.0", "rxjs-marbles": "^5.0.6", "sass-loader": "^10.2.0", - "selenium-webdriver": "^4.1.2", + "selenium-webdriver": "^4.2.0", "shelljs": "^0.8.4", "simple-git": "1.116.0", "sinon": "^7.4.2", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index f8c070b31328d..8f814fd9e7a3a 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -18,6 +18,10 @@ filegroup( "//packages/core/base/core-base-common-internal:build", "//packages/core/base/core-base-common:build", "//packages/core/base/core-base-server-internal:build", + "//packages/core/injected-metadata/core-injected-metadata-browser-internal:build", + "//packages/core/injected-metadata/core-injected-metadata-browser-mocks:build", + "//packages/core/injected-metadata/core-injected-metadata-browser:build", + "//packages/core/injected-metadata/core-injected-metadata-common-internal:build", "//packages/elastic-apm-synthtrace:build", "//packages/elastic-safer-lodash-set:build", "//packages/kbn-ace:build", @@ -117,6 +121,7 @@ filegroup( "//packages/kbn-ui-shared-deps-npm:build", "//packages/kbn-ui-shared-deps-src:build", "//packages/kbn-ui-theme:build", + "//packages/kbn-utility-types-jest:build", "//packages/kbn-utility-types:build", "//packages/kbn-utils:build", "//packages/shared-ux/avatar/solution:build", @@ -140,6 +145,10 @@ filegroup( "//packages/core/base/core-base-common-internal:build_types", "//packages/core/base/core-base-common:build_types", "//packages/core/base/core-base-server-internal:build_types", + "//packages/core/injected-metadata/core-injected-metadata-browser-internal:build_types", + "//packages/core/injected-metadata/core-injected-metadata-browser-mocks:build_types", + "//packages/core/injected-metadata/core-injected-metadata-browser:build_types", + "//packages/core/injected-metadata/core-injected-metadata-common-internal:build_types", "//packages/elastic-apm-synthtrace:build_types", "//packages/elastic-safer-lodash-set:build_types", "//packages/kbn-ace:build_types", @@ -224,6 +233,7 @@ filegroup( "//packages/kbn-ui-shared-deps-npm:build_types", "//packages/kbn-ui-shared-deps-src:build_types", "//packages/kbn-ui-theme:build_types", + "//packages/kbn-utility-types-jest:build_types", "//packages/kbn-utility-types:build_types", "//packages/kbn-utils:build_types", "//packages/shared-ux/avatar/solution:build_types", diff --git a/packages/core/base/core-base-common-internal/BUILD.bazel b/packages/core/base/core-base-common-internal/BUILD.bazel index 9eb3de99f77c6..9095c8da9f311 100644 --- a/packages/core/base/core-base-common-internal/BUILD.bazel +++ b/packages/core/base/core-base-common-internal/BUILD.bazel @@ -26,29 +26,10 @@ NPM_MODULE_EXTRA_FILES = [ "package.json", ] -# In this array place runtime dependencies, including other packages and NPM packages -# which must be available for this code to run. -# -# To reference other packages use: -# "//repo/relative/path/to/package" -# eg. "//packages/kbn-utils" -# -# To reference a NPM package use: -# "@npm//name-of-package" -# eg. "@npm//lodash" RUNTIME_DEPS = [ "@npm//react" ] -# In this array place dependencies necessary to build the types, which will include the -# :npm_module_types target of other packages and packages from NPM, including @types/* -# packages. -# -# To reference the types for another package use: -# "//repo/relative/path/to/package:npm_module_types" -# eg. "//packages/kbn-utils:npm_module_types" -# -# References to NPM packages work the same as RUNTIME_DEPS TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/jest", diff --git a/packages/core/base/core-base-common/BUILD.bazel b/packages/core/base/core-base-common/BUILD.bazel index b9c1910856d2c..118e7dbd8f2be 100644 --- a/packages/core/base/core-base-common/BUILD.bazel +++ b/packages/core/base/core-base-common/BUILD.bazel @@ -25,31 +25,13 @@ NPM_MODULE_EXTRA_FILES = [ "package.json", ] -# In this array place runtime dependencies, including other packages and NPM packages -# which must be available for this code to run. -# -# To reference other packages use: -# "//repo/relative/path/to/package" -# eg. "//packages/kbn-utils" -# -# To reference a NPM package use: -# "@npm//name-of-package" -# eg. "@npm//lodash" RUNTIME_DEPS = [ ] -# In this array place dependencies necessary to build the types, which will include the -# :npm_module_types target of other packages and packages from NPM, including @types/* -# packages. -# -# To reference the types for another package use: -# "//repo/relative/path/to/package:npm_module_types" -# eg. "//packages/kbn-utils:npm_module_types" -# -# References to NPM packages work the same as RUNTIME_DEPS TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/jest", + "//packages/kbn-config:npm_module_types", ] jsts_transpiler( diff --git a/packages/core/base/core-base-common/src/index.ts b/packages/core/base/core-base-common/src/index.ts index e1bb28de40f51..7524448e91c20 100644 --- a/packages/core/base/core-base-common/src/index.ts +++ b/packages/core/base/core-base-common/src/index.ts @@ -6,4 +6,5 @@ * Side Public License, v 1. */ -export type { PluginOpaqueId, PluginName } from './plugins'; +export type { PluginOpaqueId, PluginName, DiscoveredPlugin } from './plugins'; +export { PluginType } from './plugins'; diff --git a/packages/core/base/core-base-common/src/plugins.ts b/packages/core/base/core-base-common/src/plugins.ts index 7fe34d79e0e21..61e44374d514d 100644 --- a/packages/core/base/core-base-common/src/plugins.ts +++ b/packages/core/base/core-base-common/src/plugins.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { ConfigPath } from '@kbn/config'; + /** * Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays * that use it as a key or value more obvious. @@ -16,3 +18,68 @@ export type PluginName = string; /** @public */ export type PluginOpaqueId = symbol; + +/** @public */ +export enum PluginType { + /** + * Preboot plugins are special-purpose plugins that only function during preboot stage. + */ + preboot = 'preboot', + /** + * Standard plugins are plugins that start to function as soon as Kibana is fully booted and are active until it shuts down. + */ + standard = 'standard', +} + +/** + * Small container object used to expose information about discovered plugins that may + * or may not have been started. + * @public + */ +export interface DiscoveredPlugin { + /** + * Identifier of the plugin. + */ + readonly id: PluginName; + + /** + * Root configuration path used by the plugin, defaults to "id" in snake_case format. + */ + readonly configPath: ConfigPath; + + /** + * Type of the plugin, defaults to `standard`. + */ + readonly type: PluginType; + + /** + * An optional list of the other plugins that **must be** installed and enabled + * for this plugin to function properly. + */ + readonly requiredPlugins: readonly PluginName[]; + + /** + * An optional list of the other plugins that if installed and enabled **may be** + * leveraged by this plugin for some additional functionality but otherwise are + * not required for this plugin to work properly. + */ + readonly optionalPlugins: readonly PluginName[]; + + /** + * List of plugin ids that this plugin's UI code imports modules from that are + * not in `requiredPlugins`. + * + * @remarks + * The plugins listed here will be loaded in the browser, even if the plugin is + * disabled. Required by `@kbn/optimizer` to support cross-plugin imports. + * "core" and plugins already listed in `requiredPlugins` do not need to be + * duplicated here. + */ + readonly requiredBundles: readonly PluginName[]; + + /** + * Specifies whether this plugin - and its required dependencies - will be enabled for anonymous pages (login page, status page when + * configured, etc.) Default is false. + */ + readonly enabledOnAnonymousPages?: boolean; +} diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/BUILD.bazel b/packages/core/injected-metadata/core-injected-metadata-browser-internal/BUILD.bazel new file mode 100644 index 0000000000000..a83771bb84744 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/BUILD.bazel @@ -0,0 +1,109 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-injected-metadata-browser-internal" +PKG_REQUIRE_NAME = "@kbn/core-injected-metadata-browser-internal" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//lodash", + "//packages/kbn-std", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/lodash", + "//packages/kbn-std:npm_module_types", + "//packages/core/base/core-base-common:npm_module_types", + "//packages/core/injected-metadata/core-injected-metadata-common-internal:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/README.md b/packages/core/injected-metadata/core-injected-metadata-browser-internal/README.md new file mode 100644 index 0000000000000..9945379eceebf --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/README.md @@ -0,0 +1,3 @@ +# @kbn/core-injected-metadata-browser-internal + +This package contains the implementation and internal types of the browser-side injectedMedata service. diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/jest.config.js b/packages/core/injected-metadata/core-injected-metadata-browser-internal/jest.config.js new file mode 100644 index 0000000000000..0d957a7a3d5a2 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/injected-metadata/core-injected-metadata-browser-internal'], +}; diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/package.json b/packages/core/injected-metadata/core-injected-metadata-browser-internal/package.json new file mode 100644 index 0000000000000..3fcac1793fcad --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-injected-metadata-browser-internal", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/src/core/public/injected_metadata/index.ts b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/index.ts similarity index 80% rename from src/core/public/injected_metadata/index.ts rename to packages/core/injected-metadata/core-injected-metadata-browser-internal/src/index.ts index b2c4413ed5ff3..2fa0580e742f5 100644 --- a/src/core/public/injected_metadata/index.ts +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/index.ts @@ -5,10 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + export { InjectedMetadataService } from './injected_metadata_service'; export type { + InternalInjectedMetadataSetup, + InternalInjectedMetadataStart, InjectedMetadataParams, - InjectedMetadataSetup, - InjectedMetadataStart, - InjectedPluginMetadata, -} from './injected_metadata_service'; +} from './types'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.test.ts similarity index 99% rename from src/core/public/injected_metadata/injected_metadata_service.test.ts rename to packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.test.ts index ba0e2470d7f26..00e73a8a6fe8c 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DiscoveredPlugin } from '../../server'; +import type { DiscoveredPlugin } from '@kbn/core-base-common'; import { InjectedMetadataService } from './injected_metadata_service'; describe('setup.getElasticsearchInfo()', () => { diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.ts b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.ts new file mode 100644 index 0000000000000..9c6ef8dea4982 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { get } from 'lodash'; +import { deepFreeze } from '@kbn/std'; +import type { InjectedMetadata } from '@kbn/core-injected-metadata-common-internal'; +import type { + InjectedMetadataParams, + InternalInjectedMetadataSetup, + InternalInjectedMetadataStart, +} from './types'; + +/** + * Provides access to the metadata that is injected by the + * server into the page. The metadata is actually defined + * in the entry file for the bundle containing the new platform + * and is read from the DOM in most cases. + * + * @internal + */ +export class InjectedMetadataService { + private state: InjectedMetadata; + + constructor(private readonly params: InjectedMetadataParams) { + this.state = deepFreeze(this.params.injectedMetadata) as InjectedMetadata; + } + + public start(): InternalInjectedMetadataSetup { + return this.setup(); + } + + public setup(): InternalInjectedMetadataStart { + return { + getBasePath: () => { + return this.state.basePath; + }, + + getServerBasePath: () => { + return this.state.serverBasePath; + }, + + getPublicBaseUrl: () => { + return this.state.publicBaseUrl; + }, + + getAnonymousStatusPage: () => { + return this.state.anonymousStatusPage; + }, + + getKibanaVersion: () => { + return this.state.version; + }, + + getCspConfig: () => { + return this.state.csp; + }, + + getExternalUrlConfig: () => { + return this.state.externalUrl; + }, + + getPlugins: () => { + return this.state.uiPlugins; + }, + + getLegacyMetadata: () => { + return this.state.legacyMetadata; + }, + + getInjectedVar: (name: string, defaultValue?: any): unknown => { + return get(this.state.vars, name, defaultValue); + }, + + getInjectedVars: () => { + return this.state.vars; + }, + + getKibanaBuildNumber: () => { + return this.state.buildNumber; + }, + + getKibanaBranch: () => { + return this.state.branch; + }, + + getTheme: () => { + return this.state.theme; + }, + + getElasticsearchInfo: () => { + return this.state.clusterInfo; + }, + }; + } +} diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/types.ts b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/types.ts new file mode 100644 index 0000000000000..679673bd2b0d4 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/types.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ThemeVersion } from '@kbn/ui-shared-deps-npm'; +import { + InjectedMetadata, + InjectedMetadataClusterInfo, + InjectedMetadataExternalUrlPolicy, + InjectedMetadataPlugin, +} from '@kbn/core-injected-metadata-common-internal'; + +/** @internal */ +export interface InjectedMetadataParams { + injectedMetadata: InjectedMetadata; +} + +/** + * Provides access to the metadata injected by the server into the page + * + * @internal + */ +export interface InternalInjectedMetadataSetup { + getBasePath: () => string; + getServerBasePath: () => string; + getPublicBaseUrl: () => string | undefined; + getKibanaBuildNumber: () => number; + getKibanaBranch: () => string; + getKibanaVersion: () => string; + getCspConfig: () => { + warnLegacyBrowsers: boolean; + }; + getExternalUrlConfig: () => { + policy: InjectedMetadataExternalUrlPolicy[]; + }; + getTheme: () => { + darkMode: boolean; + version: ThemeVersion; + }; + getElasticsearchInfo: () => InjectedMetadataClusterInfo; + /** + * An array of frontend plugins in topological order. + */ + getPlugins: () => InjectedMetadataPlugin[]; + getAnonymousStatusPage: () => boolean; + getLegacyMetadata: () => { + uiSettings: { + defaults: Record; + user?: Record | undefined; + }; + }; + getInjectedVar: (name: string, defaultValue?: any) => unknown; + getInjectedVars: () => { + [key: string]: unknown; + }; +} + +/** @internal */ +export type InternalInjectedMetadataStart = InternalInjectedMetadataSetup; diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/tsconfig.json b/packages/core/injected-metadata/core-injected-metadata-browser-internal/tsconfig.json new file mode 100644 index 0000000000000..dc20b641b1989 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/BUILD.bazel b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/BUILD.bazel new file mode 100644 index 0000000000000..f4788208b932d --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/BUILD.bazel @@ -0,0 +1,105 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-injected-metadata-browser-mocks" +PKG_REQUIRE_NAME = "@kbn/core-injected-metadata-browser-mocks" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "//packages/core/injected-metadata/core-injected-metadata-browser-internal:npm_module_types", + +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/README.md b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/README.md new file mode 100644 index 0000000000000..2fa98ee0d1ca0 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/README.md @@ -0,0 +1,3 @@ +# @kbn/core-injected-metadata-browser-mocks + +This package contains the public and internal mocks for the injected medata service. diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/jest.config.js b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/jest.config.js new file mode 100644 index 0000000000000..6ffcfd6c82a74 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/injected-metadata/core-injected-metadata-browser-mocks'], +}; diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/package.json b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/package.json new file mode 100644 index 0000000000000..e8a4db9f1e47c --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-injected-metadata-browser-mocks", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/index.ts similarity index 69% rename from src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts rename to packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/index.ts index cb3fb360befeb..c10e5837765f8 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.mock.ts +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/index.ts @@ -6,7 +6,4 @@ * Side Public License, v 1. */ -export const getUpgradeableConfigMock = jest.fn(); -jest.doMock('./get_upgradeable_config', () => ({ - getUpgradeableConfig: getUpgradeableConfigMock, -})); +export { injectedMetadataServiceMock } from './injected_metadata_service.mock'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts similarity index 91% rename from src/core/public/injected_metadata/injected_metadata_service.mock.ts rename to packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts index 83903942df53d..ca510df64c1e2 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts @@ -7,10 +7,13 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { InjectedMetadataService, InjectedMetadataSetup } from './injected_metadata_service'; +import type { + InjectedMetadataService, + InternalInjectedMetadataSetup, +} from '@kbn/core-injected-metadata-browser-internal'; const createSetupContractMock = () => { - const setupContract: jest.Mocked = { + const setupContract: jest.Mocked = { getBasePath: jest.fn(), getServerBasePath: jest.fn(), getPublicBaseUrl: jest.fn(), diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/tsconfig.json b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/tsconfig.json new file mode 100644 index 0000000000000..dc20b641b1989 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/core/injected-metadata/core-injected-metadata-browser/BUILD.bazel b/packages/core/injected-metadata/core-injected-metadata-browser/BUILD.bazel new file mode 100644 index 0000000000000..ace4ba52be72b --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser/BUILD.bazel @@ -0,0 +1,124 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-injected-metadata-browser" +PKG_REQUIRE_NAME = "@kbn/core-injected-metadata-browser" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//react" +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react" +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/injected-metadata/core-injected-metadata-browser/README.md b/packages/core/injected-metadata/core-injected-metadata-browser/README.md new file mode 100644 index 0000000000000..1008081481fd0 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser/README.md @@ -0,0 +1,3 @@ +# @kbn/core-injected-metadata-browser + +This package contains the browser public types for the injectedMedata core service. diff --git a/packages/core/injected-metadata/core-injected-metadata-browser/jest.config.js b/packages/core/injected-metadata/core-injected-metadata-browser/jest.config.js new file mode 100644 index 0000000000000..211b9925953bd --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/injected-metadata/core-injected-metadata-browser'], +}; diff --git a/packages/core/injected-metadata/core-injected-metadata-browser/package.json b/packages/core/injected-metadata/core-injected-metadata-browser/package.json new file mode 100644 index 0000000000000..df5f22e5bf0d9 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-injected-metadata-browser", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/core/injected-metadata/core-injected-metadata-browser/src/contract.ts b/packages/core/injected-metadata/core-injected-metadata-browser/src/contract.ts new file mode 100644 index 0000000000000..ac06c4e75310a --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser/src/contract.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 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. + */ + +/** + * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done + * use *only* to retrieve config values. There is no way to set injected values + * in the new platform. + * @public + * @deprecated + * @removeBy 8.8.0 + */ +export interface InjectedMetadataSetup { + getInjectedVar: (name: string, defaultValue?: any) => unknown; +} + +/** + * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done + * use *only* to retrieve config values. There is no way to set injected values + * in the new platform. + * @public + * @deprecated + * @removeBy 8.8.0 + */ +export interface InjectedMetadataStart { + getInjectedVar: (name: string, defaultValue?: any) => unknown; +} diff --git a/packages/core/injected-metadata/core-injected-metadata-browser/src/index.ts b/packages/core/injected-metadata/core-injected-metadata-browser/src/index.ts new file mode 100644 index 0000000000000..827fb634bb932 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +export type { InjectedMetadataStart, InjectedMetadataSetup } from './contract'; diff --git a/packages/core/injected-metadata/core-injected-metadata-browser/tsconfig.json b/packages/core/injected-metadata/core-injected-metadata-browser/tsconfig.json new file mode 100644 index 0000000000000..dc20b641b1989 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-browser/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/BUILD.bazel b/packages/core/injected-metadata/core-injected-metadata-common-internal/BUILD.bazel new file mode 100644 index 0000000000000..49bf853fea8de --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/BUILD.bazel @@ -0,0 +1,108 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-injected-metadata-common-internal" +PKG_REQUIRE_NAME = "@kbn/core-injected-metadata-common-internal" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//react" +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "//packages/kbn-config:npm_module_types", + "//packages/kbn-ui-shared-deps-npm:npm_module_types", + "//packages/core/base/core-base-common:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/README.md b/packages/core/injected-metadata/core-injected-metadata-common-internal/README.md new file mode 100644 index 0000000000000..1066ac932eaa8 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/README.md @@ -0,0 +1,3 @@ +# @kbn/core-injected-metadata-common-internal + +This package contains the common internal types for the injectedMedata core domain. diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/jest.config.js b/packages/core/injected-metadata/core-injected-metadata-common-internal/jest.config.js new file mode 100644 index 0000000000000..86617de17a2d9 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/injected-metadata/core-injected-metadata-common-internal'], +}; diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/package.json b/packages/core/injected-metadata/core-injected-metadata-common-internal/package.json new file mode 100644 index 0000000000000..0d1d3b9866f0c --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-injected-metadata-common-internal", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/index.ts b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/index.ts new file mode 100644 index 0000000000000..7809f1b4ec38a --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/index.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. + */ + +export type { + InjectedMetadata, + InjectedMetadataClusterInfo, + InjectedMetadataExternalUrlPolicy, + InjectedMetadataPlugin, +} from './types'; diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts new file mode 100644 index 0000000000000..77d7640f2ea17 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { PluginName, DiscoveredPlugin } from '@kbn/core-base-common'; +import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; +import type { EnvironmentMode, PackageInfo } from '@kbn/config'; + +/** @internal */ +export interface InjectedMetadataClusterInfo { + cluster_uuid?: string; + cluster_name?: string; + cluster_version?: string; +} + +/** @internal */ +export interface InjectedMetadataPlugin { + id: PluginName; + plugin: DiscoveredPlugin; + config?: { + [key: string]: unknown; + }; +} + +/** @internal */ +export interface InjectedMetadataExternalUrlPolicy { + allow: boolean; + host?: string; + protocol?: string; +} + +/** @internal */ +export interface InjectedMetadata { + version: string; + buildNumber: number; + branch: string; + basePath: string; + serverBasePath: string; + publicBaseUrl?: string; + clusterInfo: InjectedMetadataClusterInfo; + env: { + mode: EnvironmentMode; + packageInfo: PackageInfo; + }; + anonymousStatusPage: boolean; + i18n: { + translationsUrl: string; + }; + theme: { + darkMode: boolean; + version: ThemeVersion; + }; + csp: { + warnLegacyBrowsers: boolean; + }; + externalUrl: { policy: InjectedMetadataExternalUrlPolicy[] }; + vars: Record; + uiPlugins: InjectedMetadataPlugin[]; + legacyMetadata: { + uiSettings: { + defaults: Record; // unreferencing UiSettingsParams here + user: Record; // unreferencing UserProvidedValues here + }; + }; +} diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/tsconfig.json b/packages/core/injected-metadata/core-injected-metadata-common-internal/tsconfig.json new file mode 100644 index 0000000000000..dc20b641b1989 --- /dev/null +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/elastic-safer-lodash-set/LICENSE b/packages/elastic-safer-lodash-set/LICENSE index ca79374b42cec..bae69c938a74c 100644 --- a/packages/elastic-safer-lodash-set/LICENSE +++ b/packages/elastic-safer-lodash-set/LICENSE @@ -7,13 +7,6 @@ Copyright (c) JS Foundation and other contributors Lodash is based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at the following locations: - - https://github.com/lodash/lodash - - https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash - - https://github.com/elastic/kibana/tree/main/packages/elastic-safer-lodash-set - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -32,3 +25,10 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at the following locations: + - https://github.com/lodash/lodash + - https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash + - https://github.com/elastic/kibana/tree/main/packages/elastic-safer-lodash-set diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel index 4b45e34b7e9fa..e672f98d5b81d 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -21,7 +21,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", - "README.md" ] RUNTIME_DEPS = [ diff --git a/packages/kbn-cli-dev-mode/README.md b/packages/kbn-cli-dev-mode/README.md deleted file mode 100644 index 6ce41249674ce..0000000000000 --- a/packages/kbn-cli-dev-mode/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# `CliDevMode` - -A class that manages the alternate behavior of the Kibana cli when using the `--dev` flag. This mode provides several useful features in a single CLI for a nice developer experience: - - - automatic server restarts when code changes - - runs the `@kbn/optimizer` to build browser bundles - - runs a base path proxy which helps developers test that they are writing code which is compatible with custom basePath settings while they work - - pauses requests when the server or optimizer are not ready to handle requests so that when users load Kibana in the browser it's always using the code as it exists on disk - -To accomplish this, and to make it easier to test, the `CliDevMode` class manages several objects: - -## `Watcher` - -The `Watcher` manages a [chokidar](https://github.com/paulmillr/chokidar) instance to watch the server files, logs about file changes observed and provides an observable to the `DevServer` via its `serverShouldRestart$()` method. - -## `DevServer` - -The `DevServer` object is responsible for everything related to running and restarting the Kibana server process: - - listens to restart notifications from the `Watcher` object, sending `SIGKILL` to the existing server and launching a new instance with the current code - - writes the stdout/stderr logs from the Kibana server to the parent process - - gracefully kills the process if the SIGINT signal is sent - - kills the server if the SIGTERM signal is sent, process.exit() is used, a second SIGINT is sent, or the gracefull shutdown times out - - proxies SIGHUP notifications to the child process, though the core team is working on migrating this functionality to the KP and making this unnecessary - -## `Optimizer` - -The `Optimizer` object manages a `@kbn/optimizer` instance, adapting its configuration and logging to the data available to the CLI. - -## `BasePathProxyServer` - -This proxy injects a random three character base path in the URL that Kibana is served from to help ensure that Kibana features -are written to adapt to custom base path configurations from users. - -The basePathProxy also has another important job, ensuring that requests don't fail because the server is restarting and -that the browser receives front-end assets containing all saved changes. We accomplish this by observing the ready state of -the `Optimizer` and `DevServer` objects and pausing all requests through the proxy until both objects report that -they aren't building/restarting based on recently saved changes. \ No newline at end of file diff --git a/packages/kbn-cli-dev-mode/README.mdx b/packages/kbn-cli-dev-mode/README.mdx new file mode 100644 index 0000000000000..487dd19d7433e --- /dev/null +++ b/packages/kbn-cli-dev-mode/README.mdx @@ -0,0 +1,57 @@ +--- +id: kibDevDocsOpsCliDevMode +slug: /kibana-dev-docs/ops/cli-dev-mode +title: "@kbn/cli-dev-mode" +description: A package to manage the Kibana cli behavior when in development +date: 2022-05-24 +tags: ['kibana', 'dev', 'contributor', 'operations', 'cli', 'dev', 'mode'] +--- + +This package exposes a function that manages the alternate behavior of the Kibana cli when using +the `--dev` flag. This mode provides several useful features in a single CLI for a nice developer +experience: + + - automatic server restarts when code changes + - runs the `@kbn/optimizer` to build browser bundles + - runs a base path proxy which helps developers test that they are writing code which is +compatible with custom basePath settings while they work + - pauses requests when the server or optimizer are not ready to handle requests so that when +users load Kibana in the browser it's always using the code as it exists on disk + +To accomplish this, and to make it easier to test, the `CliDevMode` class manages the following +objects. + +## `Watcher` + +The `Watcher` manages a [chokidar](https://github.com/paulmillr/chokidar) instance to watch the +server files, logs about file changes observed and provides an observable to the `DevServer` via +its `serverShouldRestart$()` method. + +## `DevServer` + +The `DevServer` object is responsible for everything related to running and restarting the Kibana +server process: + - listens to restart notifications from the `Watcher` object, sending `SIGKILL` to the existing +server and launching a new instance with the current code + - writes the stdout/stderr logs from the Kibana server to the parent process + - gracefully kills the process if the SIGINT signal is sent + - kills the server if the SIGTERM signal is sent, process.exit() is used, a second SIGINT is +sent, or the graceful shutdown times out + - proxies SIGHUP notifications to the child process, though the core team is working on +migrating this functionality to the KP and making this unnecessary + +## `Optimizer` + +The `Optimizer` object manages a `@kbn/optimizer` instance, adapting its configuration and +logging to the data available to the CLI. + +## `BasePathProxyServer` + +This proxy injects a random three character base path in the URL that Kibana is served from to +help ensure that Kibana features are written to adapt to custom base path configurations from users. + +The basePathProxy also has another important job, ensuring that requests don't fail because the +server is restarting and that the browser receives front-end assets containing all saved +changes. We accomplish this by observing the ready state of the `Optimizer` and `DevServer` +objects and pausing all requests through the proxy until both objects report that they +aren't building/restarting based on recently saved changes. \ No newline at end of file diff --git a/packages/kbn-es-archiver/src/actions/save.ts b/packages/kbn-es-archiver/src/actions/save.ts index 16f0cbc3c1846..9fcbe45946eb7 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 2d4b16d718689..e564bcbb1a703 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 e102ac50c3876..386d6d4a088ce 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 40907bd0af238..6e3310a7347e7 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 5dc9b4b7bd8dd..c1bb94ee13498 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 749bfd0872353..40e1c1932aeee 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 ee37591e1f2c3..8a857fb24002a 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 0000000000000..b8f5330663ee1 --- /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 0000000000000..9d67add9757db --- /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 c60c920100174..1bfbc80f52a19 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 615555b405e44..15efa53921743 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 2ab53a2ca012c..38f4bed755262 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 0000000000000..6aa68db4216f4 --- /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 241d4a8944546..4917deab542d4 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 450d575181529..c7633465ccc4c 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 fbd351cea63a9..566760b0ddf88 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 e3efaa2851609..de32e93e27398 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 506507ba0b9e6..901664988d165 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 69ab06454f93b..9ded38a6f2b58 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 9ff16d57b8661..1b533a18acade 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-query/src/es_query/types.ts b/packages/kbn-es-query/src/es_query/types.ts index f746e652d2183..14e091ed5b7f2 100644 --- a/packages/kbn-es-query/src/es_query/types.ts +++ b/packages/kbn-es-query/src/es_query/types.ts @@ -14,17 +14,19 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; */ export type IFieldSubType = IFieldSubTypeMultiOptional | IFieldSubTypeNestedOptional; -export interface IFieldSubTypeMultiOptional { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IFieldSubTypeMultiOptional = { multi?: { parent: string }; -} +}; export interface IFieldSubTypeMulti { multi: { parent: string }; } -export interface IFieldSubTypeNestedOptional { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IFieldSubTypeNestedOptional = { nested?: { path: string }; -} +}; export interface IFieldSubTypeNested { nested: { path: string }; @@ -34,7 +36,8 @@ export interface IFieldSubTypeNested { * A base interface for an index pattern field * @public */ -export interface DataViewFieldBase { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DataViewFieldBase = { name: string; /** * Kibana field type @@ -46,22 +49,23 @@ export interface DataViewFieldBase { */ script?: string; /** - * Scripted field langauge + * Scripted field language * Painless is the only valid scripted field language */ lang?: estypes.ScriptLanguage; scripted?: boolean; -} +}; /** * A base interface for an index pattern * @public */ -export interface DataViewBase { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DataViewBase = { fields: DataViewFieldBase[]; id?: string; title: string; -} +}; export interface BoolQuery { must: estypes.QueryDslQueryContainer[]; diff --git a/packages/kbn-es/BUILD.bazel b/packages/kbn-es/BUILD.bazel index 2ea9c32858dd3..892cd43244de7 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 80850c9e6a09c..a5392504490fe 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 070f11b8b5f84..b4a15a0645cce 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/packages/kbn-eslint-config/.eslintrc.js b/packages/kbn-eslint-config/.eslintrc.js index 4fd29b8b3672e..5a9d49934c255 100644 --- a/packages/kbn-eslint-config/.eslintrc.js +++ b/packages/kbn-eslint-config/.eslintrc.js @@ -102,6 +102,11 @@ module.exports = { to: '@kbn/test-jest-helpers', disallowedMessage: `import from @kbn/test-jest-helpers instead` }, + { + from: '@kbn/utility-types/jest', + to: '@kbn/utility-types-jest', + disallowedMessage: `import from @kbn/utility-types-jest instead` + }, ], ], diff --git a/packages/kbn-field-types/src/kbn_field_types_factory.ts b/packages/kbn-field-types/src/kbn_field_types_factory.ts index 2ded968c3cdf4..02f7c7444862d 100644 --- a/packages/kbn-field-types/src/kbn_field_types_factory.ts +++ b/packages/kbn-field-types/src/kbn_field_types_factory.ts @@ -21,6 +21,7 @@ export const createKbnFieldTypes = (): KbnFieldType[] => [ esTypes: [ ES_FIELD_TYPES.STRING, ES_FIELD_TYPES.TEXT, + ES_FIELD_TYPES.MATCH_ONLY_TEXT, ES_FIELD_TYPES.KEYWORD, ES_FIELD_TYPES.VERSION, ES_FIELD_TYPES._TYPE, diff --git a/packages/kbn-field-types/src/types.ts b/packages/kbn-field-types/src/types.ts index c14e7e4b03661..0addc2c5bf077 100644 --- a/packages/kbn-field-types/src/types.ts +++ b/packages/kbn-field-types/src/types.ts @@ -23,6 +23,7 @@ export enum ES_FIELD_TYPES { STRING = 'string', TEXT = 'text', + MATCH_ONLY_TEXT = 'match_only_text', KEYWORD = 'keyword', VERSION = 'version', diff --git a/packages/kbn-handlebars/LICENSE b/packages/kbn-handlebars/LICENSE index 55b4f257a1e98..5d971a1754fea 100644 --- a/packages/kbn-handlebars/LICENSE +++ b/packages/kbn-handlebars/LICENSE @@ -3,12 +3,6 @@ The MIT License (MIT) Copyright (c) Elasticsearch BV Copyright (c) Copyright (C) 2011-2019 by Yehuda Katz -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at the following locations: - - https://github.com/handlebars-lang/handlebars.js - - https://github.com/elastic/kibana/tree/main/packages/kbn-handlebars - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -27,3 +21,9 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at the following locations: + - https://github.com/handlebars-lang/handlebars.js + - https://github.com/elastic/kibana/tree/main/packages/kbn-handlebars diff --git a/packages/kbn-plugin-discovery/README.md b/packages/kbn-plugin-discovery/README.md deleted file mode 100644 index 7b433b0fdec72..0000000000000 --- a/packages/kbn-plugin-discovery/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/plugin-discovery - -Logic used to find plugins in the repository. diff --git a/packages/kbn-plugin-discovery/README.mdx b/packages/kbn-plugin-discovery/README.mdx new file mode 100644 index 0000000000000..973db3fb2a1e1 --- /dev/null +++ b/packages/kbn-plugin-discovery/README.mdx @@ -0,0 +1,24 @@ +--- +id: kibDevDocsOpsPluginDiscovery +slug: /kibana-dev-docs/ops/plugin-discovery +title: "@kbn/plugin-discovery" +description: A package with logic used to find plugins in the repository +date: 2022-06-06 +tags: ['kibana', 'dev', 'contributor', 'operations', 'plugin', 'discovery'] +--- + +At the moment plugins can live in a couple of different places and in the future will be able +to live anywhere in the repository. This is a package that holds custom logic useful to find and +parse those. + +## parseKibanaPlatformPlugin + +It returns a platform plugin for a given manifest path + +## getPluginSearchPaths + +It returns the paths where plugins will be searched for + +## simpleKibanaPlatformPluginDiscovery + +It finds and returns the new platform plugins diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap index d13765d21af1f..dbdb652a10c1f 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -36,7 +36,11 @@ exports[`ElasticAgentCard renders 1`] = ` href="/app/integrations/browse" rel="noreferrer" > - Add Elastic Agent + + Add Elastic Agent +
- Card title + + Card title +
- Card title + + Card title +
- Card title + + Card title +
- Card title + + Card title +
- Card title + + Card title +
= ({ canAccessFleet, title = elasticAgentCardTitle, + description, ...cardRest }) => { const props = canAccessFleet ? { title, - description: elasticAgentCardDescription, + description: description || elasticAgentCardDescription, } : { title: {noPermissionTitle}, diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.stories.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.stories.tsx index 84cbfb1c73a94..a87da6ff9ca0e 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.stories.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.stories.tsx @@ -31,6 +31,10 @@ PureComponent.argTypes = { control: 'boolean', defaultValue: true, }, + description: { + control: 'text', + defaultValue: '', + }, }; export const ConnectedComponent = () => { diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx index 2eda3d4c3491a..ed0b4471fa32c 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx @@ -58,6 +58,17 @@ describe('ElasticAgentCard', () => { }); }); + describe('description', () => { + test('renders custom description if provided', () => { + const component = mount( + + ); + expect(component.find(ElasticAgentCardComponent).props().description).toBe( + 'Build seamless search experiences faster.' + ); + }); + }); + describe('canAccessFleet', () => { test('passes in the right parameter', () => { const component = mount(); diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index 3702dd4a456a7..a4025f33616ed 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -20,7 +20,7 @@ export const ElasticAgentCard = (props: ElasticAgentCardProps) => { const { navigateToUrl, currentAppId$ } = useApplication(); const currentAppId = useObservable(currentAppId$); - const { href: srcHref, category } = props; + const { href: srcHref, category, description } = props; const href = useMemo(() => { if (srcHref) { @@ -39,7 +39,7 @@ export const ElasticAgentCard = (props: ElasticAgentCardProps) => { return ( - + ); }; diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/no_data_card.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/no_data_card.tsx index 705eb0444bb8d..508d03d5028b8 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/no_data_card.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/no_data_card.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiButton, EuiCard } from '@elastic/eui'; +import { EuiButton, EuiCard, EuiScreenReaderOnly } from '@elastic/eui'; import type { NoDataCardProps } from './types'; import { NoDataCardStyles } from './no_data_card.styles'; @@ -21,7 +21,7 @@ const defaultDescription = i18n.translate( ); export const NoDataCard: FunctionComponent = ({ - title, + title: titleProp, button, description, isDisabled, @@ -39,10 +39,18 @@ export const NoDataCard: FunctionComponent = ({ return button; } // Default footer action is a button with the provided or default string - return {button || title}; + return {button || titleProp}; }; + const cardDescription = description || defaultDescription; + // Fix the need for an a11y title even though the button exists by setting to screen reader only + const title = titleProp ? ( + + {titleProp} + + ) : null; + return ( ; +export type NoDataPageActions = ElasticAgentCardProps; export interface NoDataPageProps extends CommonProps { /** diff --git a/packages/kbn-utility-types-jest/BUILD.bazel b/packages/kbn-utility-types-jest/BUILD.bazel new file mode 100644 index 0000000000000..beb01908abe3e --- /dev/null +++ b/packages/kbn-utility-types-jest/BUILD.bazel @@ -0,0 +1,95 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_BASE_NAME = "kbn-utility-types-jest" +PKG_REQUIRE_NAME = "@kbn/utility-types-jest" + +SOURCE_FILES = glob([ + "src/index.ts", +]) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-utility-types-jest/README.md b/packages/kbn-utility-types-jest/README.md new file mode 100644 index 0000000000000..496630464e862 --- /dev/null +++ b/packages/kbn-utility-types-jest/README.md @@ -0,0 +1,20 @@ +# `@kbn/utility-types-jest` + +TypeScript Jest utility types for usage in Kibana. +You can add as much as any other types you think that makes sense to add here. + + +## Usage + +```ts +import type { MockedKeys } from '@kbn/utility-types-jest'; + +type A = MockedKeys; +``` + + +## Reference + +- `DeeplyMockedKeys` +- `MockedKeys` + diff --git a/packages/kbn-utility-types-jest/package.json b/packages/kbn-utility-types-jest/package.json new file mode 100644 index 0000000000000..808dd51dec793 --- /dev/null +++ b/packages/kbn-utility-types-jest/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/utility-types-jest", + "version": "1.0.0", + "private": true, + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "target_node/index.js", + "kibana": { + "devOnly": false + } +} \ No newline at end of file diff --git a/packages/kbn-utility-types/src/jest/index.ts b/packages/kbn-utility-types-jest/src/index.ts similarity index 100% rename from packages/kbn-utility-types/src/jest/index.ts rename to packages/kbn-utility-types-jest/src/index.ts diff --git a/packages/kbn-utility-types-jest/tsconfig.json b/packages/kbn-utility-types-jest/tsconfig.json new file mode 100644 index 0000000000000..1d7104a6fc254 --- /dev/null +++ b/packages/kbn-utility-types-jest/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./target_types", + "rootDir": "./src", + "stripInternal": true, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*", + ] +} diff --git a/packages/kbn-utility-types/BUILD.bazel b/packages/kbn-utility-types/BUILD.bazel index c556751d7550e..a990350cc4afc 100644 --- a/packages/kbn-utility-types/BUILD.bazel +++ b/packages/kbn-utility-types/BUILD.bazel @@ -6,7 +6,6 @@ PKG_BASE_NAME = "kbn-utility-types" PKG_REQUIRE_NAME = "@kbn/utility-types" SOURCE_FILES = glob([ - "src/jest/index.ts", "src/serializable/**", "src/index.ts" ]) @@ -19,9 +18,7 @@ filegroup( ) NPM_MODULE_EXTRA_FILES = [ - "jest/package.json", "package.json", - "README.md", ] RUNTIME_DEPS = [ @@ -30,8 +27,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "@npm//utility-types", - "@npm//@types/node", - "@npm//@types/jest", + "@npm//@types/node" ] jsts_transpiler( @@ -65,7 +61,7 @@ js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, deps = RUNTIME_DEPS + [":target_node"], - package_name = "@kbn/utility-types", + package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-utility-types/jest/package.json b/packages/kbn-utility-types/jest/package.json deleted file mode 100644 index a5d6c9ef48887..0000000000000 --- a/packages/kbn-utility-types/jest/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "types": "../target_types/jest/index.d.ts" -} diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index 1d7104a6fc254..db0a3994f6ff7 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -7,7 +7,6 @@ "rootDir": "./src", "stripInternal": true, "types": [ - "jest", "node" ] }, diff --git a/packages/shared-ux/prompt/no_data_views/BUILD.bazel b/packages/shared-ux/prompt/no_data_views/BUILD.bazel index fbfd4dcb4dfea..99e752358a000 100644 --- a/packages/shared-ux/prompt/no_data_views/BUILD.bazel +++ b/packages/shared-ux/prompt/no_data_views/BUILD.bazel @@ -10,6 +10,7 @@ SOURCE_FILES = glob( "src/**/*.ts", "src/**/*.tsx", "src/**/*.mdx", + "src/**/*.svg", ], exclude = [ "**/*.test.*", @@ -86,6 +87,10 @@ jsts_transpiler( srcs = SRCS, build_pkg_name = package_name(), web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], ) ts_config( diff --git a/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.svg b/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.svg new file mode 100644 index 0000000000000..4f499534a5ec2 --- /dev/null +++ b/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.svg @@ -0,0 +1 @@ + diff --git a/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.tsx b/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.tsx index 8a889a9267dee..55781fb13b444 100644 --- a/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.tsx +++ b/packages/shared-ux/prompt/no_data_views/src/data_view_illustration.tsx @@ -8,545 +8,22 @@ import React from 'react'; import { useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/css'; +import { css as emotion } from '@emotion/css'; + +import svg from './data_view_illustration.svg'; export const DataViewIllustration = () => { const { euiTheme } = useEuiTheme(); const { colors } = euiTheme; - const dataViewIllustrationVerticalStripes = css` - fill: ${colors.fullShade}; - `; - - const dataViewIllustrationDots = css` - fill: ${colors.lightShade}; + const css = emotion` + .dataViewIllustrationVerticalStripes { + fill: ${colors.fullShade}; + }; + .dataViewIllustrationDots { + fill: ${colors.lightShade}; + } `; - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + return Data view illustration; }; diff --git a/src/core/public/analytics/analytics_service.ts b/src/core/public/analytics/analytics_service.ts index b55dc85e5296b..b774109e79d61 100644 --- a/src/core/public/analytics/analytics_service.ts +++ b/src/core/public/analytics/analytics_service.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ +import { of } from 'rxjs'; import type { AnalyticsClient } from '@kbn/analytics-client'; import { createAnalytics } from '@kbn/analytics-client'; import type { CoreContext } from '@kbn/core-base-browser-internal'; -import { of } from 'rxjs'; +import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; import { trackClicks } from './track_clicks'; -import { InjectedMetadataSetup } from '../injected_metadata'; import { getSessionId } from './get_session_id'; import { createLogger } from './logger'; @@ -33,7 +33,7 @@ export type AnalyticsServiceStart = Pick< /** @internal */ export interface AnalyticsServiceSetupDeps { - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; } export class AnalyticsService { @@ -173,7 +173,7 @@ export class AnalyticsService { * @param injectedMetadata The injected metadata service. * @private */ - private registerElasticsearchInfoContext(injectedMetadata: InjectedMetadataSetup) { + private registerElasticsearchInfoContext(injectedMetadata: InternalInjectedMetadataSetup) { this.analyticsClient.registerContextProvider({ name: 'elasticsearch info', context$: of(injectedMetadata.getElasticsearchInfo()), diff --git a/src/core/public/apm_system.test.ts b/src/core/public/apm_system.test.ts index 0a3a1dee63e57..68ea7b8dfe5db 100644 --- a/src/core/public/apm_system.test.ts +++ b/src/core/public/apm_system.test.ts @@ -7,7 +7,7 @@ */ jest.mock('@elastic/apm-rum'); -import type { DeeplyMockedKeys, MockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys, MockedKeys } from '@kbn/utility-types-jest'; import { init, apm } from '@elastic/apm-rum'; import type { Transaction } from '@elastic/apm-rum'; import { ApmSystem } from './apm_system'; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 08c14e955bcbe..ec1cfca7d65d1 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -8,7 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { ChromeBadge, ChromeBreadcrumb, ChromeService, InternalChromeStart } from '.'; const createStartContractMock = () => { diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index dbdc867ea33b3..664b6a3ee1617 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -10,11 +10,11 @@ import { shallow } from 'enzyme'; import React from 'react'; import * as Rx from 'rxjs'; import { toArray } from 'rxjs/operators'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { App, PublicAppInfo } from '../application'; import { applicationServiceMock } from '../application/application_service.mock'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { ChromeService } from './chrome_service'; diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index a9678613b5fd0..f631819b26e4e 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -12,11 +12,11 @@ import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } import { flatMap, map, takeUntil } from 'rxjs/operators'; import { parse } from 'url'; import { EuiLink } from '@elastic/eui'; +import type { InternalInjectedMetadataStart } from '@kbn/core-injected-metadata-browser-internal'; import { mountReactNode } from '../utils/mount'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; import { HttpStart } from '../http'; -import { InjectedMetadataStart } from '../injected_metadata'; import { NotificationsStart } from '../notifications'; import { KIBANA_ASK_ELASTIC_LINK } from './constants'; import { ChromeDocTitle, DocTitleService } from './doc_title'; @@ -47,7 +47,7 @@ export interface StartDeps { application: InternalApplicationStart; docLinks: DocLinksStart; http: HttpStart; - injectedMetadata: InjectedMetadataStart; + injectedMetadata: InternalInjectedMetadataStart; notifications: NotificationsStart; } diff --git a/src/core/public/core_app/core_app.ts b/src/core/public/core_app/core_app.ts index 30a9515a2709b..5c328096c36f2 100644 --- a/src/core/public/core_app/core_app.ts +++ b/src/core/public/core_app/core_app.ts @@ -8,6 +8,7 @@ import { UnregisterCallback } from 'history'; import type { CoreContext } from '@kbn/core-base-browser-internal'; +import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; import { InternalApplicationSetup, InternalApplicationStart, @@ -17,7 +18,6 @@ import { import type { HttpSetup, HttpStart } from '../http'; import type { NotificationsSetup, NotificationsStart } from '../notifications'; import type { IUiSettingsClient } from '../ui_settings'; -import type { InjectedMetadataSetup } from '../injected_metadata'; import { renderApp as renderErrorApp, setupPublicBaseUrlConfigWarning, @@ -29,7 +29,7 @@ import { DocLinksStart } from '../doc_links'; export interface SetupDeps { application: InternalApplicationSetup; http: HttpSetup; - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; notifications: NotificationsSetup; } diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index ff24cc8839794..b0accbcd3df70 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from './http/http_service.mock'; import { i18nServiceMock } from './i18n/i18n_service.mock'; -import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; import { notificationServiceMock } from './notifications/notifications_service.mock'; import { overlayServiceMock } from './overlays/overlay_service.mock'; import { pluginsServiceMock } from './plugins/plugins_service.mock'; @@ -40,7 +40,7 @@ export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); export const InjectedMetadataServiceConstructor = jest .fn() .mockImplementation(() => MockInjectedMetadataService); -jest.doMock('./injected_metadata', () => ({ +jest.doMock('@kbn/core-injected-metadata-browser-internal', () => ({ InjectedMetadataService: InjectedMetadataServiceConstructor, })); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 0ad746f32e490..cf2840eb4f0ad 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -7,17 +7,17 @@ */ import type { CoreContext } from '@kbn/core-base-browser-internal'; +import { + InjectedMetadataService, + InjectedMetadataParams, + InternalInjectedMetadataSetup, + InternalInjectedMetadataStart, +} from '@kbn/core-injected-metadata-browser-internal'; import { CoreSetup, CoreStart } from '.'; import { ChromeService } from './chrome'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; import { HttpService } from './http'; import { I18nService } from './i18n'; -import { - InjectedMetadataParams, - InjectedMetadataService, - InjectedMetadataSetup, - InjectedMetadataStart, -} from './injected_metadata'; import { NotificationsService } from './notifications'; import { OverlayService } from './overlays'; import { PluginsService } from './plugins'; @@ -45,13 +45,13 @@ interface Params { /** @internal */ export interface InternalCoreSetup extends Omit { application: InternalApplicationSetup; - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; } /** @internal */ export interface InternalCoreStart extends Omit { application: InternalApplicationStart; - injectedMetadata: InjectedMetadataStart; + injectedMetadata: InternalInjectedMetadataStart; } /** diff --git a/src/core/public/doc_links/doc_links_service.mock.ts b/src/core/public/doc_links/doc_links_service.mock.ts index a4dd00eec84cc..b88007d68ba17 100644 --- a/src/core/public/doc_links/doc_links_service.mock.ts +++ b/src/core/public/doc_links/doc_links_service.mock.ts @@ -7,7 +7,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { DocLinksService, DocLinksStart } from './doc_links_service'; const createStartContractMock = (): DocLinksStart => { diff --git a/src/core/public/doc_links/doc_links_service.test.ts b/src/core/public/doc_links/doc_links_service.test.ts index 9ddb0fc884b9b..b30082e62c653 100644 --- a/src/core/public/doc_links/doc_links_service.test.ts +++ b/src/core/public/doc_links/doc_links_service.test.ts @@ -8,7 +8,7 @@ import { getDocLinksMock, getDocLinksMetaMock } from './doc_links_service.test.mocks'; import { DocLinksService } from './doc_links_service'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; describe('DocLinksService', () => { let injectedMetadata: ReturnType; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 09f997028e6d8..7da4737c0d5e1 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -8,10 +8,10 @@ import { getDocLinks, getDocLinksMeta } from '@kbn/doc-links'; import type { DocLinks } from '@kbn/doc-links'; -import { InjectedMetadataSetup } from '../injected_metadata'; +import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; export interface StartDeps { - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; } /** @internal */ diff --git a/src/core/public/fatal_errors/fatal_errors_service.test.ts b/src/core/public/fatal_errors/fatal_errors_service.test.ts index 4b243979c8e4d..f95b036c36df3 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.test.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.test.ts @@ -14,7 +14,7 @@ expect.addSnapshotSerializer({ }); import { mockRender } from './fatal_errors_service.test.mocks'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { themeServiceMock } from '../theme/theme_service.mock'; import { FatalErrorsService } from './fatal_errors_service'; diff --git a/src/core/public/fatal_errors/fatal_errors_service.tsx b/src/core/public/fatal_errors/fatal_errors_service.tsx index 0e72b99bc6b92..36d5ebf5c02e9 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.tsx +++ b/src/core/public/fatal_errors/fatal_errors_service.tsx @@ -11,8 +11,8 @@ import { render } from 'react-dom'; import * as Rx from 'rxjs'; import { first, tap } from 'rxjs/operators'; +import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; import { I18nStart } from '../i18n'; -import { InjectedMetadataSetup } from '../injected_metadata'; import { ThemeServiceSetup } from '../theme'; import { CoreContextProvider } from '../utils'; import { FatalErrorsScreen } from './fatal_errors_screen'; @@ -21,7 +21,7 @@ import { FatalErrorInfo, getErrorInfo } from './get_error_info'; export interface Deps { i18n: I18nStart; theme: ThemeServiceSetup; - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; } /** diff --git a/src/core/public/http/external_url_service.ts b/src/core/public/http/external_url_service.ts index e478e49552444..0e5994a253ace 100644 --- a/src/core/public/http/external_url_service.ts +++ b/src/core/public/http/external_url_service.ts @@ -7,14 +7,14 @@ */ import type { CoreService } from '@kbn/core-base-browser-internal'; +import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; import { IExternalUrlPolicy } from '../../server/types'; import { IExternalUrl } from './types'; -import { InjectedMetadataSetup } from '../injected_metadata'; import { Sha256 } from '../utils'; interface SetupDeps { location: Pick; - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; } function* getHostHashes(actualHost: string) { diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 698fa876433d4..af6e2343d5f8a 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -11,7 +11,7 @@ import fetchMock from 'fetch-mock/es5/client'; import { loadingServiceMock } from './http_service.test.mocks'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { HttpService } from './http_service'; import { Observable } from 'rxjs'; import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 5e0c2dd6def0a..4507b808e5a4a 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -7,8 +7,8 @@ */ import type { CoreService } from '@kbn/core-base-browser-internal'; +import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; import { HttpSetup, HttpStart } from './types'; -import { InjectedMetadataSetup } from '../injected_metadata'; import { FatalErrorsSetup } from '../fatal_errors'; import { BasePath } from './base_path'; import { AnonymousPathsService } from './anonymous_paths_service'; @@ -18,7 +18,7 @@ import { ExternalUrlService } from './external_url_service'; import { ExecutionContextSetup } from '../execution_context'; interface HttpDeps { - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; fatalErrors: FatalErrorsSetup; executionContext: ExecutionContextSetup; } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 378427bbd7cd3..21003b17928dd 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -26,6 +26,10 @@ import './index.scss'; +import type { + InjectedMetadataSetup, + InjectedMetadataStart, +} from '@kbn/core-injected-metadata-browser'; import { ChromeBadge, ChromeBreadcrumb, @@ -243,16 +247,8 @@ export interface CoreSetup unknown; - }; + /** {@link InjectedMetadataSetup} */ + injectedMetadata: InjectedMetadataSetup; /** {@link ThemeServiceSetup} */ theme: ThemeServiceSetup; /** {@link StartServicesAccessor} */ @@ -309,16 +305,8 @@ export interface CoreStart { deprecations: DeprecationsServiceStart; /** {@link ThemeServiceStart} */ theme: ThemeServiceStart; - /** - * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done - * use *only* to retrieve config values. There is no way to set injected values - * in the new platform. - * @deprecated - * @removeBy 8.8.0 - * */ - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; + /** {@link InjectedMetadataStart} */ + injectedMetadata: InjectedMetadataStart; } export type { diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts deleted file mode 100644 index 2aa5bbc01de89..0000000000000 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ /dev/null @@ -1,197 +0,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 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 { get } from 'lodash'; -import { deepFreeze } from '@kbn/std'; -import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; -import type { EnvironmentMode, PackageInfo } from '@kbn/config'; -import { DiscoveredPlugin, PluginName } from '../../server'; -import { IExternalUrlPolicy, UiSettingsParams, UserProvidedValues } from '../../server/types'; -import { AppCategory } from '..'; - -export interface InjectedPluginMetadata { - id: PluginName; - plugin: DiscoveredPlugin; - config?: { - [key: string]: unknown; - }; -} - -export interface InjectedMetadataClusterInfo { - cluster_uuid?: string; - cluster_name?: string; - cluster_version?: string; -} - -/** @internal */ -export interface InjectedMetadataParams { - injectedMetadata: { - version: string; - buildNumber: number; - branch: string; - basePath: string; - serverBasePath: string; - publicBaseUrl: string; - clusterInfo: InjectedMetadataClusterInfo; - category?: AppCategory; - csp: { - warnLegacyBrowsers: boolean; - }; - externalUrl: { - policy: IExternalUrlPolicy[]; - }; - vars: { - [key: string]: unknown; - }; - theme: { - darkMode: boolean; - version: ThemeVersion; - }; - env: { - mode: Readonly; - packageInfo: Readonly; - }; - uiPlugins: InjectedPluginMetadata[]; - anonymousStatusPage: boolean; - legacyMetadata: { - uiSettings: { - defaults: Record; - user?: Record; - }; - }; - }; -} - -/** - * Provides access to the metadata that is injected by the - * server into the page. The metadata is actually defined - * in the entry file for the bundle containing the new platform - * and is read from the DOM in most cases. - * - * @internal - */ -export class InjectedMetadataService { - private state: InjectedMetadataParams['injectedMetadata']; - - constructor(private readonly params: InjectedMetadataParams) { - this.state = deepFreeze( - this.params.injectedMetadata - ) as InjectedMetadataParams['injectedMetadata']; - } - - public start(): InjectedMetadataStart { - return this.setup(); - } - - public setup(): InjectedMetadataSetup { - return { - getBasePath: () => { - return this.state.basePath; - }, - - getServerBasePath: () => { - return this.state.serverBasePath; - }, - - getPublicBaseUrl: () => { - return this.state.publicBaseUrl; - }, - - getAnonymousStatusPage: () => { - return this.state.anonymousStatusPage; - }, - - getKibanaVersion: () => { - return this.state.version; - }, - - getCspConfig: () => { - return this.state.csp; - }, - - getExternalUrlConfig: () => { - return this.state.externalUrl; - }, - - getPlugins: () => { - return this.state.uiPlugins; - }, - - getLegacyMetadata: () => { - return this.state.legacyMetadata; - }, - - getInjectedVar: (name: string, defaultValue?: any): unknown => { - return get(this.state.vars, name, defaultValue); - }, - - getInjectedVars: () => { - return this.state.vars; - }, - - getKibanaBuildNumber: () => { - return this.state.buildNumber; - }, - - getKibanaBranch: () => { - return this.state.branch; - }, - - getTheme: () => { - return this.state.theme; - }, - - getElasticsearchInfo: () => { - return this.state.clusterInfo; - }, - }; - } -} - -/** - * Provides access to the metadata injected by the server into the page - * - * @internal - */ -export interface InjectedMetadataSetup { - getBasePath: () => string; - getServerBasePath: () => string; - getPublicBaseUrl: () => string; - getKibanaBuildNumber: () => number; - getKibanaBranch: () => string; - getKibanaVersion: () => string; - getCspConfig: () => { - warnLegacyBrowsers: boolean; - }; - getExternalUrlConfig: () => { - policy: IExternalUrlPolicy[]; - }; - getTheme: () => { - darkMode: boolean; - version: ThemeVersion; - }; - getElasticsearchInfo: () => InjectedMetadataClusterInfo; - /** - * An array of frontend plugins in topological order. - */ - getPlugins: () => InjectedPluginMetadata[]; - getAnonymousStatusPage: () => boolean; - getLegacyMetadata: () => { - uiSettings: { - defaults: Record; - user?: Record | undefined; - }; - }; - getInjectedVar: (name: string, defaultValue?: any) => unknown; - getInjectedVars: () => { - [key: string]: unknown; - }; -} - -/** @internal */ -export type InjectedMetadataStart = InjectedMetadataSetup; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 4dbb00967822f..38ae84c59c095 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -8,6 +8,7 @@ import { createMemoryHistory } from 'history'; import type { CoreContext } from '@kbn/core-base-browser-internal'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; // Only import types from '.' to avoid triggering default Jest mocks. import { PluginInitializerContext, AppMountParameters } from '.'; @@ -25,11 +26,11 @@ import { notificationServiceMock } from './notifications/notifications_service.m import { overlayServiceMock } from './overlays/overlay_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; -import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; import { themeServiceMock } from './theme/theme_service.mock'; import { executionContextServiceMock } from './execution_context/execution_context_service.mock'; +export { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { docLinksServiceMock } from './doc_links/doc_links_service.mock'; export { executionContextServiceMock } from './execution_context/execution_context_service.mock'; @@ -37,7 +38,6 @@ export { analyticsServiceMock } from './analytics/analytics_service.mock'; export { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; export { httpServiceMock } from './http/http_service.mock'; export { i18nServiceMock } from './i18n/i18n_service.mock'; -export { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; export { notificationServiceMock } from './notifications/notifications_service.mock'; export { overlayServiceMock } from './overlays/overlay_service.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; diff --git a/src/core/public/notifications/notifications_service.mock.ts b/src/core/public/notifications/notifications_service.mock.ts index ddb5e74c96c77..01a2a4a1d5ac9 100644 --- a/src/core/public/notifications/notifications_service.mock.ts +++ b/src/core/public/notifications/notifications_service.mock.ts @@ -7,7 +7,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { NotificationsService, NotificationsSetup, diff --git a/src/core/public/overlays/overlay_service.mock.ts b/src/core/public/overlays/overlay_service.mock.ts index 192ea46ab7466..1769dd8265f9a 100644 --- a/src/core/public/overlays/overlay_service.mock.ts +++ b/src/core/public/overlays/overlay_service.mock.ts @@ -7,7 +7,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { OverlayService, OverlayStart } from './overlay_service'; import { overlayBannersServiceMock } from './banners/banners_service.mock'; import { overlayFlyoutServiceMock } from './flyout/flyout_service.mock'; diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index c8438a03fcec7..6531e38e3196a 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -21,7 +21,7 @@ import { PluginsServiceSetupDeps, } from './plugins_service'; -import { InjectedPluginMetadata } from '../injected_metadata'; +import type { InjectedMetadataPlugin } from '@kbn/core-injected-metadata-common-internal'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { applicationServiceMock } from '../application/application_service.mock'; import { i18nServiceMock } from '../i18n/i18n_service.mock'; @@ -29,7 +29,7 @@ import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { httpServiceMock } from '../http/http_service.mock'; import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; @@ -45,7 +45,7 @@ mockPluginInitializerProvider.mockImplementation( (pluginName) => mockPluginInitializers.get(pluginName)! ); -let plugins: InjectedPluginMetadata[]; +let plugins: InjectedMetadataPlugin[]; type DeeplyMocked = { [P in keyof T]: jest.Mocked }; diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 9c2ebe9e85640..dc0a77d096ff4 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -8,6 +8,7 @@ import type { CoreService, CoreContext } from '@kbn/core-base-browser-internal'; import type { PluginName, PluginOpaqueId } from '@kbn/core-base-common'; +import type { InjectedMetadataPlugin } from '@kbn/core-injected-metadata-common-internal'; import { PluginWrapper } from './plugin'; import { createPluginInitializerContext, @@ -15,7 +16,6 @@ import { createPluginStartContext, } from './plugin_context'; import { InternalCoreSetup, InternalCoreStart } from '../core_system'; -import { InjectedPluginMetadata } from '../injected_metadata'; /** @internal */ export type PluginsServiceSetupDeps = InternalCoreSetup; @@ -26,6 +26,7 @@ export type PluginsServiceStartDeps = InternalCoreStart; export interface PluginsServiceSetup { contracts: ReadonlyMap; } + /** @internal */ export interface PluginsServiceStart { contracts: ReadonlyMap; @@ -44,7 +45,7 @@ export class PluginsService implements CoreService(plugins.map((p) => [p.id, Symbol(p.id)])); diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 132fd586b74e7..e655ccc210303 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -9,7 +9,6 @@ import { Action } from 'history'; import { AnalyticsClient } from '@kbn/analytics-client'; import type { ButtonColor } from '@elastic/eui'; -import type { ConfigPath } from '@kbn/config'; import { ContextProviderOpts } from '@kbn/analytics-client'; import { CoreContext } from '@kbn/core-base-browser-internal'; import type { DocLinks } from '@kbn/doc-links'; @@ -28,6 +27,9 @@ import { EventTypeOpts } from '@kbn/analytics-client'; import { History as History_2 } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; +import { InjectedMetadataParams } from '@kbn/core-injected-metadata-browser-internal'; +import type { InjectedMetadataSetup } from '@kbn/core-injected-metadata-browser'; +import type { InjectedMetadataStart } from '@kbn/core-injected-metadata-browser'; import { IShipper } from '@kbn/analytics-client'; import { Location as Location_2 } from 'history'; import { LocationDescriptorObject } from 'history'; @@ -36,7 +38,6 @@ import { Observable } from 'rxjs'; import { OptInConfig } from '@kbn/analytics-client'; import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; -import { PluginName } from '@kbn/core-base-common'; import { PluginOpaqueId } from '@kbn/core-base-common'; import { default as React_2 } from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; @@ -44,7 +45,6 @@ import * as Rx from 'rxjs'; import { ShipperClassConstructor } from '@kbn/analytics-client'; import { TelemetryCounter } from '@kbn/analytics-client'; import { TelemetryCounterType } from '@kbn/analytics-client'; -import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; import { TransitionPromptHook } from 'history'; import { Type } from '@kbn/config-schema'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -407,10 +407,10 @@ export interface CoreSetup; // (undocumented) http: HttpSetup; - // @deprecated - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "InjectedMetadataSetup" + // + // (undocumented) + injectedMetadata: InjectedMetadataSetup; // (undocumented) notifications: NotificationsSetup; // (undocumented) @@ -439,10 +439,10 @@ export interface CoreStart { http: HttpStart; // (undocumented) i18n: I18nStart; - // @deprecated - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "InjectedMetadataStart" + // + // (undocumented) + injectedMetadata: InjectedMetadataStart; // (undocumented) notifications: NotificationsStart; // (undocumented) diff --git a/src/core/public/theme/theme_service.test.ts b/src/core/public/theme/theme_service.test.ts index d38ef98735a3d..9bc01dc3af26e 100644 --- a/src/core/public/theme/theme_service.test.ts +++ b/src/core/public/theme/theme_service.test.ts @@ -7,7 +7,7 @@ */ import { take } from 'rxjs/operators'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { ThemeService } from './theme_service'; describe('ThemeService', () => { diff --git a/src/core/public/theme/theme_service.ts b/src/core/public/theme/theme_service.ts index e70bd901cde80..f648727789580 100644 --- a/src/core/public/theme/theme_service.ts +++ b/src/core/public/theme/theme_service.ts @@ -8,11 +8,11 @@ import { Subject, Observable, of } from 'rxjs'; import { shareReplay, takeUntil } from 'rxjs/operators'; -import { InjectedMetadataSetup } from '../injected_metadata'; +import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; import type { CoreTheme, ThemeServiceSetup, ThemeServiceStart } from './types'; export interface SetupDeps { - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; } export class ThemeService { diff --git a/src/core/public/ui_settings/ui_settings_service.test.ts b/src/core/public/ui_settings/ui_settings_service.test.ts index 83b2c26091bfd..b4af250e5b69f 100644 --- a/src/core/public/ui_settings/ui_settings_service.test.ts +++ b/src/core/public/ui_settings/ui_settings_service.test.ts @@ -9,7 +9,7 @@ import * as Rx from 'rxjs'; import { httpServiceMock } from '../http/http_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { UiSettingsService } from './ui_settings_service'; const httpSetup = httpServiceMock.createSetupContract(); diff --git a/src/core/public/ui_settings/ui_settings_service.ts b/src/core/public/ui_settings/ui_settings_service.ts index 1a3f275aa31ed..17a5f189146fb 100644 --- a/src/core/public/ui_settings/ui_settings_service.ts +++ b/src/core/public/ui_settings/ui_settings_service.ts @@ -8,8 +8,8 @@ import { Subject } from 'rxjs'; +import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; import { HttpSetup } from '../http'; -import { InjectedMetadataSetup } from '../injected_metadata'; import { UiSettingsApi } from './ui_settings_api'; import { UiSettingsClient } from './ui_settings_client'; @@ -17,7 +17,7 @@ import { IUiSettingsClient } from './types'; export interface UiSettingsServiceDeps { http: HttpSetup; - injectedMetadata: InjectedMetadataSetup; + injectedMetadata: InternalInjectedMetadataSetup; } /** @internal */ diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts index 872054837f73d..cf48a05bc82fb 100644 --- a/src/core/server/core_context.mock.ts +++ b/src/core/server/core_context.mock.ts @@ -7,7 +7,7 @@ */ import { REPO_ROOT } from '@kbn/utils'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { Env, IConfigService } from '@kbn/config'; import { configServiceMock, getEnvOptions } from '@kbn/config-mocks'; import type { CoreContext } from '@kbn/core-base-server-internal'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index b11c571c44880..9ad6612f979c6 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -261,7 +261,7 @@ export type { LogLevel, } from '@kbn/logging'; -export { PluginType } from './plugins'; +export { PluginType } from '@kbn/core-base-common'; export type { DiscoveredPlugin, diff --git a/src/core/server/logging/appenders/rewrite/rewrite_appender.test.ts b/src/core/server/logging/appenders/rewrite/rewrite_appender.test.ts index f4ce64ee65075..47042f5646a09 100644 --- a/src/core/server/logging/appenders/rewrite/rewrite_appender.test.ts +++ b/src/core/server/logging/appenders/rewrite/rewrite_appender.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { createRewritePolicyMock, resetAllMocks } from './rewrite_appender.test.mocks'; import { rewriteAppenderMocks } from './mocks'; import { LogLevel, LogRecord, LogMeta, DisposableAppender } from '@kbn/logging'; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 45fed1fd54dbb..aedf80ca552bd 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -10,7 +10,7 @@ import { of } from 'rxjs'; import { duration } from 'moment'; import { ByteSizeValue } from '@kbn/config-schema'; import { isPromise } from '@kbn/std'; -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import type { PluginInitializerContext, CoreSetup, diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index bf78842173136..0a54899856ac1 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -12,7 +12,8 @@ import { coerce } from 'semver'; import { promisify } from 'util'; import { snakeCase } from 'lodash'; import { isConfigPath, PackageInfo } from '@kbn/config'; -import { PluginManifest, PluginType } from '../types'; +import { PluginType } from '@kbn/core-base-common'; +import { PluginManifest } from '../types'; import { PluginDiscoveryError } from './plugin_discovery_error'; import { isCamelCase } from './is_camel_case'; diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 2dbb82accc14e..9ddab175d313a 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -12,7 +12,7 @@ import { firstValueFrom, Subject } from 'rxjs'; import { isPromise } from '@kbn/std'; import { isConfigSchema } from '@kbn/config-schema'; import type { Logger } from '@kbn/logging'; - +import { PluginType } from '@kbn/core-base-common'; import { AsyncPlugin, Plugin, @@ -21,7 +21,6 @@ import { PluginInitializerContext, PluginManifest, PluginOpaqueId, - PluginType, PrebootPlugin, } from './types'; import { CorePreboot, CoreSetup, CoreStart } from '..'; diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 474c7cf08b2c8..c6088e129de73 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -17,7 +17,7 @@ import type { PackageInfo, ConfigDeprecationProvider, } from '@kbn/config'; -import type { PluginName, PluginOpaqueId } from '@kbn/core-base-common'; +import type { PluginName, PluginOpaqueId, PluginType } from '@kbn/core-base-common'; import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config'; import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config'; @@ -26,7 +26,8 @@ import { CorePreboot, CoreSetup, CoreStart } from '..'; type Maybe = T | undefined; // re-exporting for now to avoid adapting all imports, will be removed later on in the migration process -export type { PluginName, PluginOpaqueId } from '@kbn/core-base-common'; +export type { PluginName, PluginOpaqueId, DiscoveredPlugin } from '@kbn/core-base-common'; +export { PluginType } from '@kbn/core-base-common'; /** * Dedicated type for plugin configuration schema. @@ -130,18 +131,6 @@ export type MakeUsageFromSchema = { : boolean; }; -/** @public */ -export enum PluginType { - /** - * Preboot plugins are special-purpose plugins that only function during preboot stage. - */ - preboot = 'preboot', - /** - * Standard plugins are plugins that start to function as soon as Kibana is fully booted and are active until it shuts down. - */ - standard = 'standard', -} - /** @internal */ export interface PluginDependencies { asNames: ReadonlyMap; @@ -265,59 +254,6 @@ export interface PluginManifest { readonly enabledOnAnonymousPages?: boolean; } -/** - * Small container object used to expose information about discovered plugins that may - * or may not have been started. - * @public - */ -export interface DiscoveredPlugin { - /** - * Identifier of the plugin. - */ - readonly id: PluginName; - - /** - * Root configuration path used by the plugin, defaults to "id" in snake_case format. - */ - readonly configPath: ConfigPath; - - /** - * Type of the plugin, defaults to `standard`. - */ - readonly type: PluginType; - - /** - * An optional list of the other plugins that **must be** installed and enabled - * for this plugin to function properly. - */ - readonly requiredPlugins: readonly PluginName[]; - - /** - * An optional list of the other plugins that if installed and enabled **may be** - * leveraged by this plugin for some additional functionality but otherwise are - * not required for this plugin to work properly. - */ - readonly optionalPlugins: readonly PluginName[]; - - /** - * List of plugin ids that this plugin's UI code imports modules from that are - * not in `requiredPlugins`. - * - * @remarks - * The plugins listed here will be loaded in the browser, even if the plugin is - * disabled. Required by `@kbn/optimizer` to support cross-plugin imports. - * "core" and plugins already listed in `requiredPlugins` do not need to be - * duplicated here. - */ - readonly requiredBundles: readonly PluginName[]; - - /** - * Specifies whether this plugin - and its required dependencies - will be enabled for anonymous pages (login page, status page when - * configured, etc.) Default is false. - */ - readonly enabledOnAnonymousPages?: boolean; -} - /** * @internal */ diff --git a/src/core/server/rendering/index.ts b/src/core/server/rendering/index.ts index ce38cfab16de0..6cf0e2a74aa1f 100644 --- a/src/core/server/rendering/index.ts +++ b/src/core/server/rendering/index.ts @@ -8,7 +8,6 @@ export { RenderingService } from './rendering_service'; export type { - InjectedMetadata, InternalRenderingServicePreboot, InternalRenderingServiceSetup, IRenderOptions, diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 09e54c362f4f1..c665df5d84d73 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -8,15 +8,13 @@ import { i18n } from '@kbn/i18n'; import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; - -import type { EnvironmentMode, PackageInfo } from '@kbn/config'; +import type { InjectedMetadata } from '@kbn/core-injected-metadata-common-internal'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { ICspConfig } from '../csp'; import { InternalHttpServicePreboot, InternalHttpServiceSetup, KibanaRequest } from '../http'; -import { UiPlugins, DiscoveredPlugin } from '../plugins'; -import { IUiSettingsClient, UserProvidedValues } from '../ui_settings'; +import { UiPlugins } from '../plugins'; +import { IUiSettingsClient } from '../ui_settings'; import type { InternalStatusServiceSetup } from '../status'; -import { IExternalUrlPolicy } from '../external_url'; /** @internal */ export interface RenderingMetadata { @@ -31,47 +29,6 @@ export interface RenderingMetadata { injectedMetadata: InjectedMetadata; } -/** @internal */ -export interface InjectedMetadata { - version: string; - buildNumber: number; - branch: string; - basePath: string; - serverBasePath: string; - publicBaseUrl?: string; - clusterInfo: { - cluster_uuid?: string; - cluster_name?: string; - cluster_version?: string; - }; - env: { - mode: EnvironmentMode; - packageInfo: PackageInfo; - }; - anonymousStatusPage: boolean; - i18n: { - translationsUrl: string; - }; - theme: { - darkMode: boolean; - version: ThemeVersion; - }; - csp: Pick; - externalUrl: { policy: IExternalUrlPolicy[] }; - vars: Record; - uiPlugins: Array<{ - id: string; - plugin: DiscoveredPlugin; - config?: Record; - }>; - legacyMetadata: { - uiSettings: { - defaults: Record; - user: Record>; - }; - }; -} - /** @internal */ export interface RenderingPrebootDeps { http: InternalHttpServicePreboot; diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts index 533ada6078caa..84a8d94f8bf2c 100644 --- a/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.test.ts @@ -8,21 +8,11 @@ import { getIndexForTypeMock } from './unknown_object_types.test.mocks'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { deleteUnknownTypeObjects, getUnknownTypesDeprecations } from './unknown_object_types'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { SavedObjectsType } from '../..'; - -const createSearchResponse = (count: number): estypes.SearchResponse => { - return { - hits: { - total: count, - max_score: 0, - hits: new Array(count).fill({}), - }, - } as estypes.SearchResponse; -}; +import { createAggregateTypesSearchResponse } from '../migrations/actions/check_for_unknown_docs.mocks'; describe('unknown saved object types deprecation', () => { const kibanaVersion = '8.0.0'; @@ -48,7 +38,7 @@ describe('unknown saved object types deprecation', () => { describe('getUnknownTypesDeprecations', () => { beforeEach(() => { - esClient.asInternalUser.search.mockResponse(createSearchResponse(0)); + esClient.asInternalUser.search.mockResponse(createAggregateTypesSearchResponse()); }); it('calls `esClient.asInternalUser.search` with the correct parameters', async () => { @@ -62,19 +52,36 @@ describe('unknown saved object types deprecation', () => { expect(esClient.asInternalUser.search).toHaveBeenCalledTimes(1); expect(esClient.asInternalUser.search).toHaveBeenCalledWith({ index: ['foo-index', 'bar-index'], - body: { - size: 10000, - query: { - bool: { - must_not: [{ term: { type: 'foo' } }, { term: { type: 'bar' } }], + size: 0, + aggs: { + typesAggregation: { + terms: { + missing: '__UNKNOWN__', + field: 'type', + size: 1000, }, + aggs: { + docs: { + top_hits: { + size: 100, + _source: { + excludes: ['*'], + }, + }, + }, + }, + }, + }, + query: { + bool: { + must_not: [{ term: { type: 'foo' } }, { term: { type: 'bar' } }], }, }, }); }); it('returns no deprecation if no unknown type docs are found', async () => { - esClient.asInternalUser.search.mockResponse(createSearchResponse(0)); + esClient.asInternalUser.search.mockResponse(createAggregateTypesSearchResponse()); const deprecations = await getUnknownTypesDeprecations({ esClient, @@ -87,7 +94,13 @@ describe('unknown saved object types deprecation', () => { }); it('returns a deprecation if any unknown type docs are found', async () => { - esClient.asInternalUser.search.mockResponse(createSearchResponse(1)); + esClient.asInternalUser.search.mockResponse( + createAggregateTypesSearchResponse({ + someType: ['id1', 'id2'], + anotherType: ['id3'], + __UNKNOWN__: ['id4'], + }) + ); const deprecations = await getUnknownTypesDeprecations({ esClient, @@ -125,6 +138,7 @@ describe('unknown saved object types deprecation', () => { }); expect(esClient.asInternalUser.deleteByQuery).toHaveBeenCalledTimes(1); + expect(esClient.asInternalUser.deleteByQuery).toHaveBeenCalledWith({ index: ['foo-index', 'bar-index'], wait_for_completion: false, diff --git a/src/core/server/saved_objects/deprecations/unknown_object_types.ts b/src/core/server/saved_objects/deprecations/unknown_object_types.ts index 28dbc4dcd41fa..9e48c84860b48 100644 --- a/src/core/server/saved_objects/deprecations/unknown_object_types.ts +++ b/src/core/server/saved_objects/deprecations/unknown_object_types.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { i18n } from '@kbn/i18n'; import type { DeprecationsDetails } from '../../deprecations'; import { IScopedClusterClient } from '../../elasticsearch'; +import { getAggregatedTypesDocuments } from '../migrations/actions/check_for_unknown_docs'; +import { addExcludedTypesToBoolQuery } from '../migrations/model/helpers'; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; -import { SavedObjectsRawDocSource } from '../serialization'; import { getIndexForType } from '../service/lib'; interface UnknownTypesDeprecationOptions { @@ -49,16 +49,6 @@ const getTargetIndices = ({ ]; }; -const getUnknownTypesQuery = (knownTypes: string[]): estypes.QueryDslQueryContainer => { - return { - bool: { - must_not: knownTypes.map((type) => ({ - term: { type }, - })), - }, - }; -}; - const getUnknownSavedObjects = async ({ typeRegistry, esClient, @@ -72,18 +62,12 @@ const getUnknownSavedObjects = async ({ kibanaIndex, kibanaVersion, }); - const query = getUnknownTypesQuery(knownTypes); - - const body = await esClient.asInternalUser.search({ - index: targetIndices, - body: { - size: 10000, - query, - }, - }); - const { hits: unknownDocs } = body.hits; - - return unknownDocs.map((doc) => ({ id: doc._id, type: doc._source?.type ?? 'unknown' })); + const excludeRegisteredTypes = addExcludedTypesToBoolQuery(knownTypes); + return await getAggregatedTypesDocuments( + esClient.asInternalUser, + targetIndices, + excludeRegisteredTypes + ); }; export const getUnknownTypesDeprecations = async ( @@ -149,13 +133,13 @@ export const deleteUnknownTypeObjects = async ({ kibanaIndex, kibanaVersion, }); - const query = getUnknownTypesQuery(knownTypes); + const nonRegisteredTypesQuery = addExcludedTypesToBoolQuery(knownTypes); await esClient.asInternalUser.deleteByQuery({ index: targetIndices, wait_for_completion: false, body: { - query, + query: nonRegisteredTypesQuery, }, }); }; diff --git a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap index 971e2d9129d47..a812c07ba3786 100644 --- a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap +++ b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap @@ -20,54 +20,9 @@ Object { "batchSize": 1000, "controlState": "LEGACY_REINDEX", "currentAlias": ".my-so-index", + "discardUnknownObjects": false, "excludeFromUpgradeFilterHooks": Object {}, - "indexPrefix": ".my-so-index", - "kibanaVersion": "7.11.0", - "knownTypes": Array [], - "legacyIndex": ".my-so-index", - "logs": Array [ - Object { - "level": "info", - "message": "Log from LEGACY_REINDEX control state", - }, - ], - "maxBatchSizeBytes": 100000000, - "migrationDocLinks": Object { - "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", - "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", - "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", - "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", - }, - "outdatedDocuments": Array [], - "outdatedDocumentsQuery": Object { - "bool": Object { - "should": Array [], - }, - }, - "preMigrationScript": Object { - "_tag": "None", - }, - "retryAttempts": 5, - "retryCount": 0, - "retryDelay": 0, - "targetIndexMappings": Object { - "properties": Object {}, - }, - "tempIndex": ".my-so-index_7.11.0_reindex_temp", - "tempIndexMappings": Object { - "dynamic": false, - "properties": Object { - "migrationVersion": Object { - "dynamic": "true", - "type": "object", - }, - "type": Object { - "type": "keyword", - }, - }, - }, - "transformedDocBatches": Array [], - "unusedTypesQuery": Object { + "excludeOnUpgradeQuery": Object { "bool": Object { "must_not": Array [ Object { @@ -164,31 +119,6 @@ Object { ], }, }, - "versionAlias": ".my-so-index_7.11.0", - "versionIndex": ".my-so-index_7.11.0_001", - }, - }, - }, - }, - ], - Array [ - "[.my-so-index] LEGACY_REINDEX RESPONSE", - Object { - "_tag": "Right", - "right": "response", - }, - ], - Array [ - "[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE. took: 0ms.", - Object { - "kibana": Object { - "migrations": Object { - "duration": 0, - "state": Object { - "batchSize": 1000, - "controlState": "LEGACY_DELETE", - "currentAlias": ".my-so-index", - "excludeFromUpgradeFilterHooks": Object {}, "indexPrefix": ".my-so-index", "kibanaVersion": "7.11.0", "knownTypes": Array [], @@ -198,10 +128,6 @@ Object { "level": "info", "message": "Log from LEGACY_REINDEX control state", }, - Object { - "level": "info", - "message": "Log from LEGACY_DELETE control state", - }, ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { @@ -239,7 +165,33 @@ Object { }, }, "transformedDocBatches": Array [], - "unusedTypesQuery": Object { + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + Array [ + "[.my-so-index] LEGACY_REINDEX RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] LEGACY_REINDEX -> LEGACY_DELETE. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "LEGACY_DELETE", + "currentAlias": ".my-so-index", + "discardUnknownObjects": false, + "excludeFromUpgradeFilterHooks": Object {}, + "excludeOnUpgradeQuery": Object { "bool": Object { "must_not": Array [ Object { @@ -336,31 +288,6 @@ Object { ], }, }, - "versionAlias": ".my-so-index_7.11.0", - "versionIndex": ".my-so-index_7.11.0_001", - }, - }, - }, - }, - ], - Array [ - "[.my-so-index] LEGACY_DELETE RESPONSE", - Object { - "_tag": "Right", - "right": "response", - }, - ], - Array [ - "[.my-so-index] LEGACY_DELETE -> LEGACY_DELETE. took: 0ms.", - Object { - "kibana": Object { - "migrations": Object { - "duration": 0, - "state": Object { - "batchSize": 1000, - "controlState": "LEGACY_DELETE", - "currentAlias": ".my-so-index", - "excludeFromUpgradeFilterHooks": Object {}, "indexPrefix": ".my-so-index", "kibanaVersion": "7.11.0", "knownTypes": Array [], @@ -374,10 +301,6 @@ Object { "level": "info", "message": "Log from LEGACY_DELETE control state", }, - Object { - "level": "info", - "message": "Log from LEGACY_DELETE control state", - }, ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { @@ -415,7 +338,33 @@ Object { }, }, "transformedDocBatches": Array [], - "unusedTypesQuery": Object { + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> LEGACY_DELETE. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "LEGACY_DELETE", + "currentAlias": ".my-so-index", + "discardUnknownObjects": false, + "excludeFromUpgradeFilterHooks": Object {}, + "excludeOnUpgradeQuery": Object { "bool": Object { "must_not": Array [ Object { @@ -512,31 +461,6 @@ Object { ], }, }, - "versionAlias": ".my-so-index_7.11.0", - "versionIndex": ".my-so-index_7.11.0_001", - }, - }, - }, - }, - ], - Array [ - "[.my-so-index] LEGACY_DELETE RESPONSE", - Object { - "_tag": "Right", - "right": "response", - }, - ], - Array [ - "[.my-so-index] LEGACY_DELETE -> DONE. took: 0ms.", - Object { - "kibana": Object { - "migrations": Object { - "duration": 0, - "state": Object { - "batchSize": 1000, - "controlState": "DONE", - "currentAlias": ".my-so-index", - "excludeFromUpgradeFilterHooks": Object {}, "indexPrefix": ".my-so-index", "kibanaVersion": "7.11.0", "knownTypes": Array [], @@ -554,10 +478,6 @@ Object { "level": "info", "message": "Log from LEGACY_DELETE control state", }, - Object { - "level": "info", - "message": "Log from DONE control state", - }, ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { @@ -595,7 +515,33 @@ Object { }, }, "transformedDocBatches": Array [], - "unusedTypesQuery": Object { + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> DONE. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "DONE", + "currentAlias": ".my-so-index", + "discardUnknownObjects": false, + "excludeFromUpgradeFilterHooks": Object {}, + "excludeOnUpgradeQuery": Object { "bool": Object { "must_not": Array [ Object { @@ -692,6 +638,64 @@ Object { ], }, }, + "indexPrefix": ".my-so-index", + "kibanaVersion": "7.11.0", + "knownTypes": Array [], + "legacyIndex": ".my-so-index", + "logs": Array [ + Object { + "level": "info", + "message": "Log from LEGACY_REINDEX control state", + }, + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + Object { + "level": "info", + "message": "Log from DONE control state", + }, + ], + "maxBatchSizeBytes": 100000000, + "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", + "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", + }, + "outdatedDocuments": Array [], + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "retryAttempts": 5, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "properties": Object {}, + }, + "tempIndex": ".my-so-index_7.11.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, + "transformedDocBatches": Array [], "versionAlias": ".my-so-index_7.11.0", "versionIndex": ".my-so-index_7.11.0_001", }, @@ -754,65 +758,9 @@ Object { "batchSize": 1000, "controlState": "LEGACY_DELETE", "currentAlias": ".my-so-index", + "discardUnknownObjects": false, "excludeFromUpgradeFilterHooks": Object {}, - "indexPrefix": ".my-so-index", - "kibanaVersion": "7.11.0", - "knownTypes": Array [], - "legacyIndex": ".my-so-index", - "logs": Array [ - Object { - "level": "info", - "message": "Log from LEGACY_DELETE control state", - }, - ], - "maxBatchSizeBytes": 100000000, - "migrationDocLinks": Object { - "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", - "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", - "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", - "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", - }, - "outdatedDocuments": Array [ - Object { - "_id": "1234", - }, - ], - "outdatedDocumentsQuery": Object { - "bool": Object { - "should": Array [], - }, - }, - "preMigrationScript": Object { - "_tag": "None", - }, - "reason": "the fatal reason", - "retryAttempts": 5, - "retryCount": 0, - "retryDelay": 0, - "targetIndexMappings": Object { - "properties": Object {}, - }, - "tempIndex": ".my-so-index_7.11.0_reindex_temp", - "tempIndexMappings": Object { - "dynamic": false, - "properties": Object { - "migrationVersion": Object { - "dynamic": "true", - "type": "object", - }, - "type": Object { - "type": "keyword", - }, - }, - }, - "transformedDocBatches": Array [ - Array [ - Object { - "_id": "1234", - }, - ], - ], - "unusedTypesQuery": Object { + "excludeOnUpgradeQuery": Object { "bool": Object { "must_not": Array [ Object { @@ -909,31 +857,6 @@ Object { ], }, }, - "versionAlias": ".my-so-index_7.11.0", - "versionIndex": ".my-so-index_7.11.0_001", - }, - }, - }, - }, - ], - Array [ - "[.my-so-index] LEGACY_DELETE RESPONSE", - Object { - "_tag": "Right", - "right": "response", - }, - ], - Array [ - "[.my-so-index] LEGACY_DELETE -> FATAL. took: 0ms.", - Object { - "kibana": Object { - "migrations": Object { - "duration": 0, - "state": Object { - "batchSize": 1000, - "controlState": "FATAL", - "currentAlias": ".my-so-index", - "excludeFromUpgradeFilterHooks": Object {}, "indexPrefix": ".my-so-index", "kibanaVersion": "7.11.0", "knownTypes": Array [], @@ -943,10 +866,6 @@ Object { "level": "info", "message": "Log from LEGACY_DELETE control state", }, - Object { - "level": "info", - "message": "Log from FATAL control state", - }, ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { @@ -995,7 +914,33 @@ Object { }, ], ], - "unusedTypesQuery": Object { + "versionAlias": ".my-so-index_7.11.0", + "versionIndex": ".my-so-index_7.11.0_001", + }, + }, + }, + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE RESPONSE", + Object { + "_tag": "Right", + "right": "response", + }, + ], + Array [ + "[.my-so-index] LEGACY_DELETE -> FATAL. took: 0ms.", + Object { + "kibana": Object { + "migrations": Object { + "duration": 0, + "state": Object { + "batchSize": 1000, + "controlState": "FATAL", + "currentAlias": ".my-so-index", + "discardUnknownObjects": false, + "excludeFromUpgradeFilterHooks": Object {}, + "excludeOnUpgradeQuery": Object { "bool": Object { "must_not": Array [ Object { @@ -1092,6 +1037,67 @@ Object { ], }, }, + "indexPrefix": ".my-so-index", + "kibanaVersion": "7.11.0", + "knownTypes": Array [], + "legacyIndex": ".my-so-index", + "logs": Array [ + Object { + "level": "info", + "message": "Log from LEGACY_DELETE control state", + }, + Object { + "level": "info", + "message": "Log from FATAL control state", + }, + ], + "maxBatchSizeBytes": 100000000, + "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", + "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", + }, + "outdatedDocuments": Array [ + Object { + "_id": "1234", + }, + ], + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "reason": "the fatal reason", + "retryAttempts": 5, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "properties": Object {}, + }, + "tempIndex": ".my-so-index_7.11.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, + "transformedDocBatches": Array [ + Array [ + Object { + "_id": "1234", + }, + ], + ], "versionAlias": ".my-so-index_7.11.0", "versionIndex": ".my-so-index_7.11.0_001", }, diff --git a/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.test.ts b/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.test.ts index 0f41233fd58f2..68bd6c934a80f 100644 --- a/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.test.ts +++ b/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.test.ts @@ -28,14 +28,10 @@ describe('calculateExcludeFilters', () => { expect(hook2).toHaveBeenCalledWith({ readonlyEsClient: { search: expect.any(Function) } }); expect(Either.isRight(result)).toBe(true); expect((result as Either.Right).right).toEqual({ - excludeFilter: { - bool: { - must_not: [ - { bool: { must: { term: { fieldA: '123' } } } }, - { bool: { must: { term: { fieldB: 'abc' } } } }, - ], - }, - }, + mustNotClauses: [ + { bool: { must: { term: { fieldA: '123' } } } }, + { bool: { must: { term: { fieldB: 'abc' } } } }, + ], errorsByType: {}, }); }); @@ -53,11 +49,7 @@ describe('calculateExcludeFilters', () => { expect(Either.isRight(result)).toBe(true); expect((result as Either.Right).right).toEqual({ - excludeFilter: { - bool: { - must_not: [{ bool: { must: { term: { fieldB: 'abc' } } } }], - }, - }, + mustNotClauses: [{ bool: { must: { term: { fieldB: 'abc' } } } }], errorsByType: { type1: error }, }); }); @@ -99,11 +91,7 @@ describe('calculateExcludeFilters', () => { expect(Either.isRight(result)).toBe(true); expect((result as Either.Right).right).toEqual({ - excludeFilter: { - bool: { - must_not: [{ bool: { must: { term: { fieldB: 'abc' } } } }], - }, - }, + mustNotClauses: [{ bool: { must: { term: { fieldB: 'abc' } } } }], errorsByType: expect.any(Object), }); expect((result as Either.Right).right.errorsByType.type1.toString()).toMatchInlineSnapshot( diff --git a/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.ts b/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.ts index 27ce7bd4c404b..a2cea776792ad 100644 --- a/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.ts +++ b/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { withTimeout } from '@kbn/std'; import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; -import { RetryableEsClientError } from '.'; -import { ElasticsearchClient } from '../../../elasticsearch'; -import { SavedObjectTypeExcludeFromUpgradeFilterHook } from '../../types'; +import type { RetryableEsClientError } from '.'; +import type { ElasticsearchClient } from '../../../elasticsearch'; +import type { SavedObjectTypeExcludeFromUpgradeFilterHook } from '../../types'; import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors'; export interface CalculateExcludeFiltersParams { @@ -22,8 +22,8 @@ export interface CalculateExcludeFiltersParams { } export interface CalculatedExcludeFilter { - /** Composite filter of all calculated filters */ - excludeFilter: estypes.QueryDslQueryContainer; + /** Array with all the clauses that must be bool.must_not'ed */ + mustNotClauses: QueryDslQueryContainer[]; /** Any errors that were encountered during filter calculation, keyed by the type name */ errorsByType: Record; } @@ -39,7 +39,7 @@ export const calculateExcludeFilters = > => () => { return Promise.all< - | Either.Right + | Either.Right | Either.Left<{ soType: string; error: Error | RetryableEsClientError }> >( Object.entries(excludeFromUpgradeFilterHooks).map(([soType, hook]) => @@ -91,22 +91,17 @@ export const calculateExcludeFilters = } const errorsByType: Array<[string, Error]> = []; - const filters: estypes.QueryDslQueryContainer[] = []; + const mustNotClauses: QueryDslQueryContainer[] = []; // Loop through all results and collect successes and errors results.forEach((r) => Either.isRight(r) - ? filters.push(r.right) + ? mustNotClauses.push(r.right) : Either.isLeft(r) && errorsByType.push([r.left.soType, r.left.error as Error]) ); - // Composite filter from all calculated filters that successfully executed - const excludeFilter: estypes.QueryDslQueryContainer = { - bool: { must_not: filters }, - }; - return Either.right({ - excludeFilter, + mustNotClauses, errorsByType: Object.fromEntries(errorsByType), }); }); diff --git a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.mocks.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.mocks.ts new file mode 100644 index 0000000000000..a762a9f18e0b6 --- /dev/null +++ b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.mocks.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; + +interface DocIdsByType { + [type: string]: string[]; +} + +export const createAggregateTypesSearchResponse = (typesIds: DocIdsByType = {}): SearchResponse => { + return { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: Object.keys(typesIds).length, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + typesAggregation: { + buckets: Object.entries(typesIds).map(([type, ids]) => ({ + key: type, + docs: { hits: { hits: ids.map((_id) => ({ _id })) } }, + })), + }, + }, + }; +}; diff --git a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts index 4254c152a1fa8..d5c99f01ada33 100644 --- a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts +++ b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts @@ -9,14 +9,15 @@ import * as Either from 'fp-ts/lib/Either'; import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors'; import { errors as EsErrors } from '@elastic/elasticsearch'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { checkForUnknownDocs } from './check_for_unknown_docs'; +import { createAggregateTypesSearchResponse } from './check_for_unknown_docs.mocks'; jest.mock('./catch_retryable_es_client_errors'); describe('checkForUnknownDocs', () => { - const unusedTypesQuery: estypes.QueryDslQueryContainer = { + const excludeOnUpgradeQuery: QueryDslQueryContainer = { bool: { must: [{ term: { hello: 'dolly' } }] }, }; const knownTypes = ['foo', 'bar']; @@ -41,7 +42,7 @@ describe('checkForUnknownDocs', () => { client, indexName: '.kibana_8.0.0', knownTypes, - unusedTypesQuery, + excludeOnUpgradeQuery, }); try { await task(); @@ -60,7 +61,7 @@ describe('checkForUnknownDocs', () => { client, indexName: '.kibana_8.0.0', knownTypes, - unusedTypesQuery, + excludeOnUpgradeQuery, }); await task(); @@ -68,18 +69,37 @@ describe('checkForUnknownDocs', () => { expect(client.search).toHaveBeenCalledTimes(1); expect(client.search).toHaveBeenCalledWith({ index: '.kibana_8.0.0', - body: { - query: { - bool: { - must: unusedTypesQuery, - must_not: knownTypes.map((type) => ({ - term: { - type, + size: 0, + aggs: { + typesAggregation: { + terms: { + // assign type __UNKNOWN__ to those documents that don't define one + missing: '__UNKNOWN__', + field: 'type', + size: 1000, // collect up to 1000 non-registered types + }, + aggs: { + docs: { + top_hits: { + size: 100, // collect up to 100 docs for each non-registered type + _source: { + excludes: ['*'], + }, }, - })), + }, }, }, }, + query: { + bool: { + ...excludeOnUpgradeQuery.bool, + must_not: knownTypes.map((type) => ({ + term: { + type, + }, + })), + }, + }, }); }); @@ -92,7 +112,7 @@ describe('checkForUnknownDocs', () => { client, indexName: '.kibana_8.0.0', knownTypes, - unusedTypesQuery, + excludeOnUpgradeQuery, }); const result = await task(); @@ -101,59 +121,36 @@ describe('checkForUnknownDocs', () => { expect((result as Either.Right).right).toEqual({}); }); - it('resolves with `Either.left` when unknown docs are found', async () => { - const client = elasticsearchClientMock.createInternalClient( - Promise.resolve({ - hits: { - hits: [ - { _id: '12', _source: { type: 'foo' } }, - { _id: '14', _source: { type: 'bar' } }, - ], - }, - }) - ); - - const task = checkForUnknownDocs({ - client, - indexName: '.kibana_8.0.0', - knownTypes, - unusedTypesQuery, - }); - - const result = await task(); - - expect(Either.isLeft(result)).toBe(true); - expect((result as Either.Left).left).toEqual({ - type: 'unknown_docs_found', - unknownDocs: [ - { id: '12', type: 'foo' }, - { id: '14', type: 'bar' }, - ], - }); - }); - - it('uses `unknown` as the type when the document does not contain a type field', async () => { - const client = elasticsearchClientMock.createInternalClient( - Promise.resolve({ - hits: { - hits: [{ _id: '12', _source: {} }], - }, - }) - ); - - const task = checkForUnknownDocs({ - client, - indexName: '.kibana_8.0.0', - knownTypes, - unusedTypesQuery, - }); - - const result = await task(); - - expect(Either.isLeft(result)).toBe(true); - expect((result as Either.Left).left).toEqual({ - type: 'unknown_docs_found', - unknownDocs: [{ id: '12', type: 'unknown' }], + describe('when unknown doc types are found', () => { + it('resolves with `Either.right`, returning the unknown doc types', async () => { + const client = elasticsearchClientMock.createInternalClient( + Promise.resolve( + createAggregateTypesSearchResponse({ + foo: ['12'], + bar: ['14'], + __UNKNOWN__: ['16'], + }) + ) + ); + + const task = checkForUnknownDocs({ + client, + indexName: '.kibana_8.0.0', + knownTypes, + excludeOnUpgradeQuery, + }); + + const result = await task(); + + expect(Either.isRight(result)).toBe(true); + expect((result as Either.Right).right).toEqual({ + type: 'unknown_docs_found', + unknownDocs: [ + { id: '12', type: 'foo' }, + { id: '14', type: 'bar' }, + { id: '16', type: '__UNKNOWN__' }, + ], + }); }); }); }); diff --git a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts index f9e3df9a0443a..b475f47e3d7f2 100644 --- a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts +++ b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts @@ -8,24 +8,31 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { flatten } from 'lodash'; +import type { + AggregationsMultiBucketAggregateBase, + Indices, + QueryDslQueryContainer, + SearchRequest, +} from '@elastic/elasticsearch/lib/api/types'; import type { SavedObjectsRawDocSource } from '../../serialization'; -import { ElasticsearchClient } from '../../../elasticsearch'; +import type { ElasticsearchClient } from '../../../elasticsearch'; import { catchRetryableEsClientErrors, - RetryableEsClientError, + type RetryableEsClientError, } from './catch_retryable_es_client_errors'; +import { addExcludedTypesToBoolQuery } from '../model/helpers'; /** @internal */ export interface CheckForUnknownDocsParams { client: ElasticsearchClient; indexName: string; - unusedTypesQuery: estypes.QueryDslQueryContainer; + excludeOnUpgradeQuery: QueryDslQueryContainer; knownTypes: string[]; } /** @internal */ -export interface CheckForUnknownDocsFoundDoc { +export interface DocumentIdAndType { id: string; type: string; } @@ -33,55 +40,90 @@ export interface CheckForUnknownDocsFoundDoc { /** @internal */ export interface UnknownDocsFound { type: 'unknown_docs_found'; - unknownDocs: CheckForUnknownDocsFoundDoc[]; + unknownDocs: DocumentIdAndType[]; +} + +/** + * Performs a search in ES, aggregating documents by type, + * retrieving a bunch of documents for each type. + * @internal + * @param esClient The ES client to perform the search query + * @param targetIndices The ES indices to target + * @param query An optional query that can be used to filter + * @returns A list of documents with their types + */ +export async function getAggregatedTypesDocuments( + esClient: ElasticsearchClient, + targetIndices: Indices, + query?: QueryDslQueryContainer +): Promise { + const params: SearchRequest = { + index: targetIndices, + size: 0, + // apply the desired filters (e.g. filter out registered types) + query, + // aggregate docs by type, so that we have a sneak peak of all types + aggs: { + typesAggregation: { + terms: { + // assign type __UNKNOWN__ to those documents that don't define one + missing: '__UNKNOWN__', + field: 'type', + size: 1000, // collect up to 1000 types + }, + aggs: { + docs: { + top_hits: { + size: 100, // collect up to 100 docs for each type + _source: { + excludes: ['*'], + }, + }, + }, + }, + }, + }, + }; + + const body = await esClient.search(params); + + if (!body.aggregations) return []; + + const { typesAggregation } = body.aggregations; + const buckets = (typesAggregation as AggregationsMultiBucketAggregateBase).buckets; + + const bucketsArray = Array.isArray(buckets) ? buckets : Object.values(buckets); + + return flatten( + bucketsArray.map( + (bucket: any) => + bucket.docs?.hits?.hits?.map((doc: any) => ({ + id: doc._id, + type: bucket.key, + })) || [] + ) + ); } export const checkForUnknownDocs = ({ client, indexName, - unusedTypesQuery, + excludeOnUpgradeQuery, knownTypes, }: CheckForUnknownDocsParams): TaskEither.TaskEither< - RetryableEsClientError | UnknownDocsFound, - {} + RetryableEsClientError, + UnknownDocsFound | {} > => - () => { - const query = createUnknownDocQuery(unusedTypesQuery, knownTypes); - - return client - .search({ - index: indexName, - body: { - query, - }, - }) - .then((body) => { - const { hits } = body.hits; - if (hits.length) { - return Either.left({ - type: 'unknown_docs_found' as const, - unknownDocs: hits.map((hit) => ({ id: hit._id, type: hit._source?.type ?? 'unknown' })), - }); - } else { - return Either.right({}); + async () => { + const excludeQuery = addExcludedTypesToBoolQuery(knownTypes, excludeOnUpgradeQuery.bool); + return getAggregatedTypesDocuments(client, indexName, excludeQuery) + .then((unknownDocs) => { + if (unknownDocs.length) { + return Either.right({ type: 'unknown_docs_found' as const, unknownDocs }); } + + return Either.right({}); }) .catch(catchRetryableEsClientErrors); }; - -const createUnknownDocQuery = ( - unusedTypesQuery: estypes.QueryDslQueryContainer, - knownTypes: string[] -): estypes.QueryDslQueryContainer => { - return { - bool: { - must: unusedTypesQuery, - must_not: knownTypes.map((type) => ({ - term: { - type, - }, - })), - }, - }; -}; diff --git a/src/core/server/saved_objects/migrations/actions/index.ts b/src/core/server/saved_objects/migrations/actions/index.ts index 3a387d764fa4c..4ac6bfa24fee6 100644 --- a/src/core/server/saved_objects/migrations/actions/index.ts +++ b/src/core/server/saved_objects/migrations/actions/index.ts @@ -93,7 +93,7 @@ import { ClusterShardLimitExceeded } from './create_index'; export type { CheckForUnknownDocsParams, UnknownDocsFound, - CheckForUnknownDocsFoundDoc, + DocumentIdAndType, } from './check_for_unknown_docs'; export { checkForUnknownDocs } from './check_for_unknown_docs'; @@ -160,7 +160,7 @@ export interface ActionErrorTypeMap { /** * Type guard for narrowing the type of a left */ -export function isLeftTypeof( +export function isTypeof( res: any, typeString: T ): res is ActionErrorTypeMap[T] { diff --git a/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts index d47d53aa367e7..a799ced0ffe39 100644 --- a/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts @@ -6,42 +6,42 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from '../../../..'; +import Path from 'path'; +import * as Either from 'fp-ts/lib/Either'; +import * as Option from 'fp-ts/lib/Option'; +import { errors } from '@elastic/elasticsearch'; +import type { TaskEither } from 'fp-ts/lib/TaskEither'; +import type { ElasticsearchClient } from '../../../..'; import * as kbnTestServer from '../../../../../test_helpers/kbn_server'; -import { SavedObjectsRawDoc } from '../../../serialization'; +import type { SavedObjectsRawDoc } from '../../../serialization'; import { bulkOverwriteTransformedDocuments, cloneIndex, closePit, createIndex, openPit, - OpenPitResponse, + type OpenPitResponse, reindex, readWithPit, - ReadWithPit, + type ReadWithPit, searchForOutdatedDocuments, SearchResponse, setWriteBlock, updateAliases, waitForReindexTask, - ReindexResponse, + type ReindexResponse, waitForPickupUpdatedMappingsTask, pickupUpdatedMappings, - UpdateByQueryResponse, + type UpdateByQueryResponse, updateAndPickupMappings, - UpdateAndPickupMappingsResponse, + type UpdateAndPickupMappingsResponse, verifyReindex, removeWriteBlock, transformDocs, waitForIndexStatusYellow, initAction, } from '..'; -import * as Either from 'fp-ts/lib/Either'; -import * as Option from 'fp-ts/lib/Option'; -import { errors } from '@elastic/elasticsearch'; -import { DocumentsTransformFailed, DocumentsTransformSuccess } from '../../core'; -import { TaskEither } from 'fp-ts/lib/TaskEither'; -import Path from 'path'; +import type { DocumentsTransformFailed, DocumentsTransformSuccess } from '../../core'; const { startES } = kbnTestServer.createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), @@ -610,7 +610,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -637,14 +637,14 @@ describe('migration actions', () => { ] `); }); - it('resolves right and excludes all documents not matching the unusedTypesQuery', async () => { + it('resolves right and excludes all documents not matching the excludeOnUpgradeQuery', async () => { const res = (await reindex({ client, sourceIndex: 'existing_index_with_docs', targetIndex: 'reindex_target_excluded_docs', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { + excludeOnUpgradeQuery: { bool: { must_not: ['f_agent_event', 'another_unused_type'].map((type) => ({ term: { type }, @@ -683,7 +683,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target_2', reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -718,7 +718,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target_3', reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; let task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -735,7 +735,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target_3', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -794,7 +794,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target_4', reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -850,7 +850,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target_5', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; const task = waitForReindexTask({ client, taskId: reindexTaskId, timeout: '10s' }); @@ -889,7 +889,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target_6', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; const task = waitForReindexTask({ client, taskId: reindexTaskId, timeout: '10s' }); @@ -910,7 +910,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { + excludeOnUpgradeQuery: { match_all: {}, }, })()) as Either.Right; @@ -933,7 +933,7 @@ describe('migration actions', () => { targetIndex: 'existing_index_with_write_block', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); @@ -955,7 +955,7 @@ describe('migration actions', () => { targetIndex: 'existing_index_with_write_block', reindexScript: Option.none, requireAlias: true, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); @@ -983,7 +983,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '0s' }); @@ -1010,7 +1010,7 @@ describe('migration actions', () => { targetIndex: 'reindex_target_7', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: { match_all: {} }, + excludeOnUpgradeQuery: { match_all: {} }, })()) as Either.Right; await waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' })(); diff --git a/src/core/server/saved_objects/migrations/actions/reindex.test.ts b/src/core/server/saved_objects/migrations/actions/reindex.test.ts index f53368bd9321b..3352e4eebadca 100644 --- a/src/core/server/saved_objects/migrations/actions/reindex.test.ts +++ b/src/core/server/saved_objects/migrations/actions/reindex.test.ts @@ -36,7 +36,7 @@ describe('reindex', () => { targetIndex: 'my_target_index', reindexScript: Option.none, requireAlias: false, - unusedTypesQuery: {}, + excludeOnUpgradeQuery: {}, }); try { await task(); diff --git a/src/core/server/saved_objects/migrations/actions/reindex.ts b/src/core/server/saved_objects/migrations/actions/reindex.ts index cfd7449971b7f..46f20928e1bef 100644 --- a/src/core/server/saved_objects/migrations/actions/reindex.ts +++ b/src/core/server/saved_objects/migrations/actions/reindex.ts @@ -9,11 +9,11 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; import * as Option from 'fp-ts/lib/Option'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient } from '../../../elasticsearch'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient } from '../../../elasticsearch'; import { catchRetryableEsClientErrors, - RetryableEsClientError, + type RetryableEsClientError, } from './catch_retryable_es_client_errors'; import { BATCH_SIZE } from './constants'; @@ -33,7 +33,7 @@ export interface ReindexParams { * are no longer used. These saved objects will still be kept in the outdated * index for backup purposes, but won't be available in the upgraded index. */ - unusedTypesQuery: estypes.QueryDslQueryContainer; + excludeOnUpgradeQuery: QueryDslQueryContainer; } /** @@ -51,7 +51,7 @@ export const reindex = targetIndex, reindexScript, requireAlias, - unusedTypesQuery, + excludeOnUpgradeQuery, }: ReindexParams): TaskEither.TaskEither => () => { return client @@ -67,7 +67,7 @@ export const reindex = // Set reindex batch size size: BATCH_SIZE, // Exclude saved object types - query: unusedTypesQuery, + query: excludeOnUpgradeQuery, }, dest: { index: targetIndex, diff --git a/src/core/server/saved_objects/migrations/core/unused_types.ts b/src/core/server/saved_objects/migrations/core/unused_types.ts index 076bdb489cf49..d80dcb23b7189 100644 --- a/src/core/server/saved_objects/migrations/core/unused_types.ts +++ b/src/core/server/saved_objects/migrations/core/unused_types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; /** * Types that are no longer registered and need to be removed @@ -41,7 +41,7 @@ export const REMOVED_TYPES: string[] = [ // saved objects which are no longer used. These saved objects will still be // kept in the outdated index for backup purposes, but won't be available in // the upgraded index. -export const excludeUnusedTypesQuery: estypes.QueryDslQueryContainer = { +export const excludeUnusedTypesQuery: QueryDslQueryContainer = { bool: { must_not: [ ...REMOVED_TYPES.map((typeName) => ({ diff --git a/src/core/server/saved_objects/migrations/initial_state.test.ts b/src/core/server/saved_objects/migrations/initial_state.test.ts index c2a2736541208..cfa9c9f3a2435 100644 --- a/src/core/server/saved_objects/migrations/initial_state.test.ts +++ b/src/core/server/saved_objects/migrations/initial_state.test.ts @@ -8,12 +8,14 @@ import { ByteSizeValue } from '@kbn/config-schema'; import * as Option from 'fp-ts/Option'; -import { DocLinksServiceSetup } from '../../doc_links'; -import { docLinksServiceMock } from '../../mocks'; -import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; +import type { DocLinksServiceSetup } from '../../doc_links'; +import { docLinksServiceMock, loggingSystemMock } from '../../mocks'; +import type { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { createInitialState } from './initial_state'; +const mockLogger = loggingSystemMock.create(); + describe('createInitialState', () => { let typeRegistry: SavedObjectTypeRegistry; let docLinks: DocLinksServiceSetup; @@ -41,62 +43,16 @@ describe('createInitialState', () => { migrationsConfig, typeRegistry, docLinks, + logger: mockLogger.get(), }) ).toMatchInlineSnapshot(` Object { "batchSize": 1000, "controlState": "INIT", "currentAlias": ".kibana_task_manager", + "discardUnknownObjects": false, "excludeFromUpgradeFilterHooks": Object {}, - "indexPrefix": ".kibana_task_manager", - "kibanaVersion": "8.1.0", - "knownTypes": Array [], - "legacyIndex": ".kibana_task_manager", - "logs": Array [], - "maxBatchSizeBytes": 104857600, - "migrationDocLinks": Object { - "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", - "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", - "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", - "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", - }, - "outdatedDocumentsQuery": Object { - "bool": Object { - "should": Array [], - }, - }, - "preMigrationScript": Object { - "_tag": "None", - }, - "retryAttempts": 15, - "retryCount": 0, - "retryDelay": 0, - "targetIndexMappings": Object { - "dynamic": "strict", - "properties": Object { - "my_type": Object { - "properties": Object { - "title": Object { - "type": "text", - }, - }, - }, - }, - }, - "tempIndex": ".kibana_task_manager_8.1.0_reindex_temp", - "tempIndexMappings": Object { - "dynamic": false, - "properties": Object { - "migrationVersion": Object { - "dynamic": "true", - "type": "object", - }, - "type": Object { - "type": "keyword", - }, - }, - }, - "unusedTypesQuery": Object { + "excludeOnUpgradeQuery": Object { "bool": Object { "must_not": Array [ Object { @@ -193,6 +149,54 @@ describe('createInitialState', () => { ], }, }, + "indexPrefix": ".kibana_task_manager", + "kibanaVersion": "8.1.0", + "knownTypes": Array [], + "legacyIndex": ".kibana_task_manager", + "logs": Array [], + "maxBatchSizeBytes": 104857600, + "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", + "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", + }, + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "retryAttempts": 15, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "dynamic": "strict", + "properties": Object { + "my_type": Object { + "properties": Object { + "title": Object { + "type": "text", + }, + }, + }, + }, + }, + "tempIndex": ".kibana_task_manager_8.1.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, + }, + }, "versionAlias": ".kibana_task_manager_8.1.0", "versionIndex": ".kibana_task_manager_8.1.0_001", } @@ -224,6 +228,7 @@ describe('createInitialState', () => { migrationsConfig, typeRegistry, docLinks, + logger: mockLogger.get(), }); expect(initialState.knownTypes).toEqual(['foo', 'bar']); @@ -250,6 +255,7 @@ describe('createInitialState', () => { migrationsConfig, typeRegistry, docLinks, + logger: mockLogger.get(), }); expect(initialState.excludeFromUpgradeFilterHooks).toEqual({ foo: fooExcludeOnUpgradeHook }); @@ -269,6 +275,7 @@ describe('createInitialState', () => { migrationsConfig, typeRegistry, docLinks, + logger: mockLogger.get(), }); expect(Option.isSome(initialState.preMigrationScript)).toEqual(true); @@ -291,6 +298,7 @@ describe('createInitialState', () => { migrationsConfig, typeRegistry, docLinks, + logger: mockLogger.get(), }).preMigrationScript ) ).toEqual(true); @@ -309,6 +317,7 @@ describe('createInitialState', () => { migrationsConfig, typeRegistry, docLinks, + logger: mockLogger.get(), }).outdatedDocumentsQuery ).toMatchInlineSnapshot(` Object { @@ -347,4 +356,71 @@ describe('createInitialState', () => { } `); }); + + it('initializes the `discardUnknownObjects` flag to false if the flag is not provided in the config', () => { + const logger = mockLogger.get(); + const initialState = createInitialState({ + kibanaVersion: '8.1.0', + targetMappings: { + dynamic: 'strict', + properties: { my_type: { properties: { title: { type: 'text' } } } }, + }, + migrationVersionPerType: {}, + indexPrefix: '.kibana_task_manager', + migrationsConfig, + typeRegistry, + docLinks, + logger, + }); + + expect(logger.warn).not.toBeCalled(); + expect(initialState.discardUnknownObjects).toEqual(false); + }); + + it('initializes the `discardUnknownObjects` flag to false if the value provided in the config does not match the current kibana version', () => { + const logger = mockLogger.get(); + const initialState = createInitialState({ + kibanaVersion: '8.1.0', + targetMappings: { + dynamic: 'strict', + properties: { my_type: { properties: { title: { type: 'text' } } } }, + }, + migrationVersionPerType: {}, + indexPrefix: '.kibana_task_manager', + migrationsConfig: { + ...migrationsConfig, + discardUnknownObjects: '8.0.0', + }, + typeRegistry, + docLinks, + logger, + }); + + expect(initialState.discardUnknownObjects).toEqual(false); + expect(logger.warn).toBeCalledTimes(1); + expect(logger.warn).toBeCalledWith( + 'The flag `migrations.discardUnknownObjects` is defined but does not match the current kibana version; unknown objects will NOT be ignored.' + ); + }); + + it('initializes the `discardUnknownObjects` flag to true if the value provided in the config matches the current kibana version', () => { + const initialState = createInitialState({ + kibanaVersion: '8.1.0', + targetMappings: { + dynamic: 'strict', + properties: { my_type: { properties: { title: { type: 'text' } } } }, + }, + migrationVersionPerType: {}, + indexPrefix: '.kibana_task_manager', + migrationsConfig: { + ...migrationsConfig, + discardUnknownObjects: '8.1.0', + }, + typeRegistry, + docLinks, + logger: mockLogger.get(), + }); + + expect(initialState.discardUnknownObjects).toEqual(true); + }); }); diff --git a/src/core/server/saved_objects/migrations/initial_state.ts b/src/core/server/saved_objects/migrations/initial_state.ts index ae0fe54049505..f576a5a93429a 100644 --- a/src/core/server/saved_objects/migrations/initial_state.ts +++ b/src/core/server/saved_objects/migrations/initial_state.ts @@ -7,13 +7,14 @@ */ import * as Option from 'fp-ts/Option'; -import { IndexMapping } from '../mappings'; -import { SavedObjectsMigrationVersion } from '../../../types'; -import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; +import type { Logger } from '@kbn/logging'; +import type { IndexMapping } from '../mappings'; +import type { SavedObjectsMigrationVersion } from '../../../types'; +import type { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import type { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; -import { InitState } from './state'; +import type { InitState } from './state'; import { excludeUnusedTypesQuery } from './core'; -import { DocLinksServiceStart } from '../../doc_links'; +import type { DocLinksServiceStart } from '../../doc_links'; /** * Construct the initial state for the model @@ -27,6 +28,7 @@ export const createInitialState = ({ migrationsConfig, typeRegistry, docLinks, + logger, }: { kibanaVersion: string; targetMappings: IndexMapping; @@ -36,6 +38,7 @@ export const createInitialState = ({ migrationsConfig: SavedObjectsMigrationConfigType; typeRegistry: ISavedObjectTypeRegistry; docLinks: DocLinksServiceStart; + logger: Logger; }): InitState => { const outdatedDocumentsQuery = { bool: { @@ -70,6 +73,15 @@ export const createInitialState = ({ // short key to access savedObjects entries directly from docLinks const migrationDocLinks = docLinks.links.kibanaUpgradeSavedObjects; + if ( + migrationsConfig.discardUnknownObjects && + migrationsConfig.discardUnknownObjects !== kibanaVersion + ) { + logger.warn( + 'The flag `migrations.discardUnknownObjects` is defined but does not match the current kibana version; unknown objects will NOT be ignored.' + ); + } + return { controlState: 'INIT', indexPrefix, @@ -88,8 +100,9 @@ export const createInitialState = ({ retryAttempts: migrationsConfig.retryAttempts, batchSize: migrationsConfig.batchSize, maxBatchSizeBytes: migrationsConfig.maxBatchSizeBytes.getValueInBytes(), + discardUnknownObjects: migrationsConfig.discardUnknownObjects === kibanaVersion, logs: [], - unusedTypesQuery: excludeUnusedTypesQuery, + excludeOnUpgradeQuery: excludeUnusedTypesQuery, knownTypes, excludeFromUpgradeFilterHooks: excludeFilterHooks, migrationDocLinks, diff --git a/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts index dc521baad6335..6ccb6497516f9 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts @@ -8,9 +8,13 @@ import Path from 'path'; import fs from 'fs/promises'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; +import { Env } from '@kbn/config'; +import { REPO_ROOT } from '@kbn/utils'; +import { getEnvOptions } from '@kbn/config-mocks'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; -import { Root } from '../../../root'; +import type { Root } from '../../../root'; +import type { ElasticsearchClient } from '../../../elasticsearch'; const logFilePath = Path.join(__dirname, '7_13_unknown_types.log'); @@ -54,62 +58,83 @@ describe('migration v2', () => { await new Promise((resolve) => setTimeout(resolve, 10000)); }); - it('fails the migration if unknown types are found in the source index', async () => { - // Start kibana with foo and space types disabled - root = createRoot(); - esServer = await startES(); - await root.preboot(); - await root.setup(); - - try { - await root.start(); - expect('should have thrown').toEqual('but it did not'); - } catch (err) { - const errorMessage = err.message; - - expect( - errorMessage.startsWith( - 'Unable to complete saved object migrations for the [.kibana] index: Migration failed because documents ' + - 'were found for unknown saved object types. To proceed with the migration, please delete these documents from the ' + - '".kibana_7.13.0_001" index.' - ) - ).toBeTruthy(); - - const unknownDocs = [ - { type: 'space', id: 'space:default' }, - { type: 'space', id: 'space:first' }, - { type: 'space', id: 'space:second' }, - { type: 'space', id: 'space:third' }, - { type: 'space', id: 'space:forth' }, - { type: 'space', id: 'space:fifth' }, - { type: 'space', id: 'space:sixth' }, - { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, - { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, - { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, - ]; - - unknownDocs.forEach(({ id, type }) => { - expect(errorMessage).toEqual(expect.stringContaining(`- "${id}" (type: "${type}")`)); - }); - - const client = esServer.es.getClient(); - const { body: response } = await client.indices.getSettings( - { index: '.kibana_7.13.0_001' }, - { meta: true } - ); - const settings = response['.kibana_7.13.0_001'].settings as estypes.IndicesIndexSettings; - expect(settings.index).not.toBeUndefined(); - expect(settings.index!.blocks?.write).not.toEqual('true'); - } + describe('when `discardUnknownObjects` does not match current kibana version', () => { + it('fails the migration if unknown types are found in the source index', async () => { + // Start kibana with foo and space types disabled + root = createRoot('7.13.0'); + esServer = await startES(); + await root.preboot(); + await root.setup(); + + try { + await root.start(); + expect('should have thrown').toEqual('but it did not'); + } catch (err) { + const errorMessage = err.message; + + expect( + errorMessage.startsWith( + 'Unable to complete saved object migrations for the [.kibana] index: Migration failed because some documents ' + + 'were found which use unknown saved object types:' + ) + ).toBeTruthy(); + + const unknownDocs = [ + { type: 'space', id: 'space:default' }, + { type: 'space', id: 'space:first' }, + { type: 'space', id: 'space:second' }, + { type: 'space', id: 'space:third' }, + { type: 'space', id: 'space:forth' }, + { type: 'space', id: 'space:fifth' }, + { type: 'space', id: 'space:sixth' }, + { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, + { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, + { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, + ]; + + unknownDocs.forEach(({ id, type }) => { + expect(errorMessage).toEqual(expect.stringContaining(`- "${id}" (type: "${type}")`)); + }); + + const client = esServer.es.getClient(); + const { body: response } = await client.indices.getSettings( + { index: '.kibana_7.13.0_001' }, + { meta: true } + ); + const settings = response['.kibana_7.13.0_001'].settings as IndicesIndexSettings; + expect(settings.index).not.toBeUndefined(); + expect(settings.index!.blocks?.write).not.toEqual('true'); + } + }); + }); + + describe('when `discardUnknownObjects` matches current kibana version', () => { + const currentVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; + + it('discards the documents with unknown types and finishes the migration successfully', async () => { + // Start kibana with foo and space types disabled + root = createRoot(currentVersion); + esServer = await startES(); + await root.preboot(); + await root.setup(); + + // the migration process should finish successfully + await expect(root.start()).resolves.not.toThrowError(); + + const esClient: ElasticsearchClient = esServer.es.getClient(); + const body = await esClient.count({ q: 'type:foo|space' }); + expect(body.count).toEqual(0); + }); }); }); -function createRoot() { +function createRoot(discardUnknownObjects?: string) { return kbnTestServer.createRootWithCorePlugins( { migrations: { skip: false, batchSize: 5, + discardUnknownObjects, }, logging: { appenders: { diff --git a/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts index 79a76d0a9f4fc..c50d29ba521e7 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts @@ -114,7 +114,7 @@ const previouslyRegisteredTypes = [ ].sort(); describe('SO type registrations', () => { - it('does not remove types from registrations without updating unusedTypesQuery', async () => { + it('does not remove types from registrations without updating excludeOnUpgradeQuery', async () => { const root = kbnTestServer.createRoot({}, { oss: false }); await root.preboot(); const setup = await root.setup(); diff --git a/src/core/server/saved_objects/migrations/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana_migrator.ts index 54feeccb9573f..90413d70c99bb 100644 --- a/src/core/server/saved_objects/migrations/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.ts @@ -118,9 +118,7 @@ export class KibanaMigrator { * The promise resolves with an array of migration statuses, one for each * elasticsearch index which was migrated. */ - public runMigrations({ rerun = false }: { rerun?: boolean } = {}): Promise< - Array<{ status: string }> - > { + public runMigrations({ rerun = false }: { rerun?: boolean } = {}): Promise { if (this.migrationResult === undefined || rerun) { // Reruns are only used by CI / EsArchiver. Publishing status updates on reruns results in slowing down CI // unnecessarily, so we skip it in this case. @@ -147,7 +145,7 @@ export class KibanaMigrator { return this.status$.asObservable(); } - private runMigrationsInternal() { + private runMigrationsInternal(): Promise { const indexMap = createIndexMap({ kibanaIndexName: this.kibanaIndex, indexMap: this.mappingProperties, diff --git a/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts index 93e6476f8e78c..f99f336a9ac74 100644 --- a/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts @@ -50,6 +50,7 @@ describe('migrationsStateActionMachine', () => { }, typeRegistry, docLinks, + logger: mockLogger.get(), }); const next = jest.fn((s: State) => { diff --git a/src/core/server/saved_objects/migrations/model/extract_errors.test.ts b/src/core/server/saved_objects/migrations/model/extract_errors.test.ts index ea6b312c2053d..036ead853cf55 100644 --- a/src/core/server/saved_objects/migrations/model/extract_errors.test.ts +++ b/src/core/server/saved_objects/migrations/model/extract_errors.test.ts @@ -8,35 +8,53 @@ import { extractUnknownDocFailureReason, + extractDiscardedUnknownDocs, fatalReasonDocumentExceedsMaxBatchSizeBytes, } from './extract_errors'; describe('extractUnknownDocFailureReason', () => { it('generates the correct error message', () => { expect( - extractUnknownDocFailureReason( - [ - { - id: 'unknownType:12', - type: 'unknownType', - }, - { - id: 'anotherUnknownType:42', - type: 'anotherUnknownType', - }, - ], - '.kibana_15' - ) + extractUnknownDocFailureReason('some-url.co', [ + { + id: 'unknownType:12', + type: 'unknownType', + }, + { + id: 'anotherUnknownType:42', + type: 'anotherUnknownType', + }, + ]) ).toMatchInlineSnapshot(` - "Migration failed because documents were found for unknown saved object types. To proceed with the migration, please delete these documents from the \\".kibana_15\\" index. - The documents with unknown types are: + "Migration failed because some documents were found which use unknown saved object types: - \\"unknownType:12\\" (type: \\"unknownType\\") - \\"anotherUnknownType:42\\" (type: \\"anotherUnknownType\\") - You can delete them using the following command: - curl -X POST \\"{elasticsearch}/.kibana_15/_bulk?pretty\\" -H 'Content-Type: application/json' -d' - { \\"delete\\" : { \\"_id\\" : \\"unknownType:12\\" } } - { \\"delete\\" : { \\"_id\\" : \\"anotherUnknownType:42\\" } } - '" + + To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration. + Please refer to some-url.co for more information." + `); + }); +}); + +describe('extractDiscardedUnknownDocs', () => { + it('generates the correct error message', () => { + expect( + extractDiscardedUnknownDocs([ + { + id: 'unknownType:12', + type: 'unknownType', + }, + { + id: 'anotherUnknownType:42', + type: 'anotherUnknownType', + }, + ]) + ).toMatchInlineSnapshot(` + "Kibana has been configured to discard unknown documents for this migration. + Therefore, the following documents with unknown types will not be taken into account and they will not be available after the migration: + - \\"unknownType:12\\" (type: \\"unknownType\\") + - \\"anotherUnknownType:42\\" (type: \\"anotherUnknownType\\") + " `); }); }); diff --git a/src/core/server/saved_objects/migrations/model/extract_errors.ts b/src/core/server/saved_objects/migrations/model/extract_errors.ts index c529e1b1da269..1d4d697b74774 100644 --- a/src/core/server/saved_objects/migrations/model/extract_errors.ts +++ b/src/core/server/saved_objects/migrations/model/extract_errors.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { TransformErrorObjects } from '../core'; -import { CheckForUnknownDocsFoundDoc } from '../actions'; +import type { TransformErrorObjects } from '../core'; +import type { DocumentIdAndType } from '../actions'; /** * Constructs migration failure message strings from corrupt document ids and document transformation errors @@ -36,19 +36,23 @@ export function extractTransformFailuresReason( ); } +export function extractDiscardedUnknownDocs(unknownDocs: DocumentIdAndType[]): string { + return ( + `Kibana has been configured to discard unknown documents for this migration.\n` + + `Therefore, the following documents with unknown types will not be taken into account and they will not be available after the migration:\n` + + unknownDocs.map((doc) => `- "${doc.id}" (type: "${doc.type}")\n`).join('') + ); +} + export function extractUnknownDocFailureReason( - unknownDocs: CheckForUnknownDocsFoundDoc[], - sourceIndex: string + resolveMigrationFailuresUrl: string, + unknownDocs: DocumentIdAndType[] ): string { return ( - `Migration failed because documents were found for unknown saved object types. ` + - `To proceed with the migration, please delete these documents from the "${sourceIndex}" index.\n` + - `The documents with unknown types are:\n` + + `Migration failed because some documents were found which use unknown saved object types:\n` + unknownDocs.map((doc) => `- "${doc.id}" (type: "${doc.type}")\n`).join('') + - `You can delete them using the following command:\n` + - `curl -X POST "{elasticsearch}/${sourceIndex}/_bulk?pretty" -H 'Content-Type: application/json' -d'\n` + - unknownDocs.map((doc) => `{ "delete" : { "_id" : "${doc.id}" } }\n`).join('') + - `'` + `\nTo proceed with the migration you can configure Kibana to discard unknown saved objects for this migration.\n` + + `Please refer to ${resolveMigrationFailuresUrl} for more information.` ); } diff --git a/src/core/server/saved_objects/migrations/model/helpers.test.ts b/src/core/server/saved_objects/migrations/model/helpers.test.ts new file mode 100644 index 0000000000000..0e2056eacac66 --- /dev/null +++ b/src/core/server/saved_objects/migrations/model/helpers.test.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { + addExcludedTypesToBoolQuery, + addMustClausesToBoolQuery, + addMustNotClausesToBoolQuery, +} from './helpers'; + +describe('addExcludedTypesToBoolQuery', () => { + it('generates a bool query which filters out the specified types', () => { + const boolQuery = { must_not: [] }; + const types = ['type1', 'type2']; + const result = addExcludedTypesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + must_not: [{ term: { type: 'type1' } }, { term: { type: 'type2' } }], + }, + }); + }); +}); + +describe('addMustClausesToBoolQuery', () => { + it('generates a new bool query when no query is provided', () => { + const boolQuery = undefined; + const types = [{ term: { type: 'type1' } }, { term: { type: 'type2' } }]; + const result = addMustClausesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + must: [{ term: { type: 'type1' } }, { term: { type: 'type2' } }], + }, + }); + }); + + it('adds a new must clause to the provided bool query, if it did exist', () => { + const boolQuery = { + should: [ + { match: { 'name.first': { query: 'shay', _name: 'first' } } }, + { match: { 'name.last': { query: 'banon', _name: 'last' } } }, + ], + }; + const types = [{ term: { type: 'type1' } }, { term: { type: 'type2' } }]; + const result = addMustClausesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + should: [ + { match: { 'name.first': { query: 'shay', _name: 'first' } } }, + { match: { 'name.last': { query: 'banon', _name: 'last' } } }, + ], + must: [{ term: { type: 'type1' } }, { term: { type: 'type2' } }], + }, + }); + }); + + it('appends the given clauses to the existing must', () => { + const boolQuery = { + must: [ + { match: { type: 'search-session' } }, + { match: { 'search-session.persisted': false } }, + ], + }; + + const types = [{ term: { type: 'type1' } }, { term: { type: 'type2' } }]; + const result = addMustClausesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + must: [ + { match: { type: 'search-session' } }, + { match: { 'search-session.persisted': false } }, + { term: { type: 'type1' } }, + { term: { type: 'type2' } }, + ], + }, + }); + }); + + it('arrayifys the existing must clause if needed', () => { + const boolQuery = { + must: { + term: { type: 'type0' }, + }, + }; + + const types = [{ term: { type: 'type1' } }, { term: { type: 'type2' } }]; + const result = addMustClausesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + must: [ + { term: { type: 'type0' } }, + { term: { type: 'type1' } }, + { term: { type: 'type2' } }, + ], + }, + }); + }); +}); + +describe('addMustNotClausesToBoolQuery', () => { + it('generates a new bool query when no query is provided', () => { + const boolQuery = undefined; + const types = [{ term: { type: 'type1' } }, { term: { type: 'type2' } }]; + const result = addMustNotClausesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + must_not: [{ term: { type: 'type1' } }, { term: { type: 'type2' } }], + }, + }); + }); + + it('adds a new must_not clause to the provided bool query, if it did not exist', () => { + const boolQuery = { + should: [ + { match: { 'name.first': { query: 'shay', _name: 'first' } } }, + { match: { 'name.last': { query: 'banon', _name: 'last' } } }, + ], + }; + const types = [{ term: { type: 'type1' } }, { term: { type: 'type2' } }]; + const result = addMustNotClausesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + should: [ + { match: { 'name.first': { query: 'shay', _name: 'first' } } }, + { match: { 'name.last': { query: 'banon', _name: 'last' } } }, + ], + must_not: [{ term: { type: 'type1' } }, { term: { type: 'type2' } }], + }, + }); + }); + + it('appends the given clauses to the existing must_not', () => { + const boolQuery = { + must_not: [ + { match: { type: 'search-session' } }, + { match: { 'search-session.persisted': false } }, + ], + }; + + const types = [{ term: { type: 'type1' } }, { term: { type: 'type2' } }]; + const result = addMustNotClausesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + must_not: [ + { match: { type: 'search-session' } }, + { match: { 'search-session.persisted': false } }, + { term: { type: 'type1' } }, + { term: { type: 'type2' } }, + ], + }, + }); + }); + + it('arrayifys the existing must_not clause if needed', () => { + const boolQuery = { + must_not: { + term: { type: 'type0' }, + }, + }; + + const types = [{ term: { type: 'type1' } }, { term: { type: 'type2' } }]; + const result = addMustNotClausesToBoolQuery(types, boolQuery); + expect(result).toEqual({ + bool: { + must_not: [ + { term: { type: 'type0' } }, + { term: { type: 'type1' } }, + { term: { type: 'type2' } }, + ], + }, + }); + }); +}); diff --git a/src/core/server/saved_objects/migrations/model/helpers.ts b/src/core/server/saved_objects/migrations/model/helpers.ts index c3a4c85679680..a139089bd3a2d 100644 --- a/src/core/server/saved_objects/migrations/model/helpers.ts +++ b/src/core/server/saved_objects/migrations/model/helpers.ts @@ -7,9 +7,13 @@ */ import { gt, valid } from 'semver'; -import { State } from '../state'; -import { IndexMapping } from '../../mappings'; -import { FetchIndexResponse } from '../actions'; +import type { + QueryDslBoolQuery, + QueryDslQueryContainer, +} from '@elastic/elasticsearch/lib/api/types'; +import type { State } from '../state'; +import type { IndexMapping } from '../../mappings'; +import type { FetchIndexResponse } from '../actions'; /** * A helper function/type for ensuring that all control state's are handled. @@ -68,6 +72,75 @@ export function indexBelongsToLaterVersion(indexName: string, kibanaVersion: str return version != null ? gt(version, kibanaVersion) : false; } +/** + * Add new must_not clauses to the given query + * in order to filter out the specified types + * @param boolQuery the bool query to be enriched + * @param types the types to be filtered out + * @returns a new query container with the enriched query + */ +export function addExcludedTypesToBoolQuery( + types: string[], + boolQuery?: QueryDslBoolQuery +): QueryDslQueryContainer { + return addMustNotClausesToBoolQuery( + types.map((type) => ({ term: { type } })), + boolQuery + ); +} + +/** + * Add the given clauses to the 'must' of the given query + * @param boolQuery the bool query to be enriched + * @param mustClauses the clauses to be added to a 'must' + * @returns a new query container with the enriched query + */ +export function addMustClausesToBoolQuery( + mustClauses: QueryDslQueryContainer[], + boolQuery?: QueryDslBoolQuery +): QueryDslQueryContainer { + let must: QueryDslQueryContainer[] = []; + + if (boolQuery?.must) { + must = must.concat(boolQuery.must); + } + + must.push(...mustClauses); + + return { + bool: { + ...boolQuery, + must, + }, + }; +} + +/** + * Add the given clauses to the 'must_not' of the given query + * @param boolQuery the bool query to be enriched + * @param mustNotClauses the clauses to be added to a 'must_not' + * @returns a new query container with the enriched query + */ +export function addMustNotClausesToBoolQuery( + mustNotClauses: QueryDslQueryContainer[], + boolQuery?: QueryDslBoolQuery +): QueryDslQueryContainer { + let mustNot: QueryDslQueryContainer[] = []; + + if (boolQuery?.must_not) { + mustNot = mustNot.concat(boolQuery.must_not); + } + + mustNot.push(...mustNotClauses); + + return { + bool: { + ...boolQuery, + must_not: mustNot, + }, + }; +} + /** * Extracts the version number from a >= 7.11 index * @param indexName A >= v7.11 index name diff --git a/src/core/server/saved_objects/migrations/model/model.test.ts b/src/core/server/saved_objects/migrations/model/model.test.ts index 1782ece9b4827..5a63e180e7aa9 100644 --- a/src/core/server/saved_objects/migrations/model/model.test.ts +++ b/src/core/server/saved_objects/migrations/model/model.test.ts @@ -59,6 +59,7 @@ describe('migrations v2 model', () => { retryAttempts: 15, batchSize: 1000, maxBatchSizeBytes: 1e8, + discardUnknownObjects: false, indexPrefix: '.kibana', outdatedDocumentsQuery: {}, targetIndexMappings: { @@ -81,7 +82,7 @@ describe('migrations v2 model', () => { versionAlias: '.kibana_7.11.0', versionIndex: '.kibana_7.11.0_001', tempIndex: '.kibana_7.11.0_reindex_temp', - unusedTypesQuery: { + excludeOnUpgradeQuery: { bool: { must_not: [ { @@ -805,7 +806,7 @@ describe('migrations v2 model', () => { }, } as const; - test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds', () => { + test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds and no unknown docs are found', () => { const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { ...baseState, controlState: 'CHECK_UNKNOWN_DOCUMENTS', @@ -853,29 +854,76 @@ describe('migrations v2 model', () => { expect(newState.logs).toEqual([]); }); - test('CHECK_UNKNOWN_DOCUMENTS -> FATAL if action fails and unknown docs were found', () => { - const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { - ...baseState, - controlState: 'CHECK_UNKNOWN_DOCUMENTS', - sourceIndex: Option.some('.kibana_3') as Option.Some, - sourceIndexMappings: mappingsWithUnknownType, - }; + describe('when unknown docs are found', () => { + test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if discardUnknownObjects=true', () => { + const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { + ...baseState, + discardUnknownObjects: true, + controlState: 'CHECK_UNKNOWN_DOCUMENTS', + sourceIndex: Option.some('.kibana_3') as Option.Some, + sourceIndexMappings: mappingsWithUnknownType, + }; + + const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({ + type: 'unknown_docs_found', + unknownDocs: [ + { id: 'dashboard:12', type: 'dashboard' }, + { id: 'foo:17', type: 'foo' }, + ], + }); + const newState = model(checkUnknownDocumentsSourceState, res); - const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.left({ - type: 'unknown_docs_found', - unknownDocs: [ - { id: 'dashboard:12', type: 'dashboard' }, - { id: 'foo:17', type: 'foo' }, - ], + expect(newState).toMatchObject({ + controlState: 'SET_SOURCE_WRITE_BLOCK', + sourceIndex: Option.some('.kibana_3'), + targetIndex: '.kibana_7.11.0_001', + }); + + expect(newState.excludeOnUpgradeQuery).toEqual({ + bool: { + must_not: [ + { term: { type: 'unused-fleet-agent-events' } }, + { term: { type: 'dashboard' } }, + { term: { type: 'foo' } }, + ], + must: [{ exists: { field: 'type' } }], + }, + }); + + // we should have a warning in the logs about the ignored types + expect( + newState.logs.find(({ level, message }) => { + return ( + level === 'warning' && message.includes('dashboard') && message.includes('foo') + ); + }) + ).toBeDefined(); }); - const newState = model(checkUnknownDocumentsSourceState, res); - expect(newState.controlState).toEqual('FATAL'); - expect(newState).toMatchObject({ - controlState: 'FATAL', - reason: expect.stringContaining( - 'Migration failed because documents were found for unknown saved object types' - ), + test('CHECK_UNKNOWN_DOCUMENTS -> FATAL if discardUnknownObjects=false', () => { + const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { + ...baseState, + controlState: 'CHECK_UNKNOWN_DOCUMENTS', + sourceIndex: Option.some('.kibana_3') as Option.Some, + sourceIndexMappings: mappingsWithUnknownType, + }; + + const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({ + type: 'unknown_docs_found', + unknownDocs: [ + { id: 'dashboard:12', type: 'dashboard' }, + { id: 'foo:17', type: 'foo' }, + ], + }); + const newState = model(checkUnknownDocumentsSourceState, res); + expect(newState.controlState).toEqual('FATAL'); + + expect(newState).toMatchObject({ + controlState: 'FATAL', + reason: expect.stringContaining( + 'Migration failed because some documents were found which use unknown saved object types' + ), + }); }); }); }); @@ -926,30 +974,27 @@ describe('migrations v2 model', () => { const newState = model(state, res); expect(newState.controlState).toEqual('CALCULATE_EXCLUDE_FILTERS'); }); - test('CALCULATE_EXCLUDE_FILTERS -> CREATE_REINDEX_TEMP if action succeeds with filters', () => { + it('CALCULATE_EXCLUDE_FILTERS -> CREATE_REINDEX_TEMP if action succeeds with filters', () => { const res: ResponseType<'CALCULATE_EXCLUDE_FILTERS'> = Either.right({ - excludeFilter: { bool: { must: { term: { fieldA: 'abc' } } } }, + mustNotClauses: [{ term: { fieldA: 'abc' } }], errorsByType: { type1: new Error('an error!') }, }); const newState = model(state, res); expect(newState.controlState).toEqual('CREATE_REINDEX_TEMP'); - expect(newState.unusedTypesQuery).toEqual({ - // New filter should be combined unused type query and filter from response + + expect(newState.excludeOnUpgradeQuery).toEqual({ + // new filters should be added inside a must_not clause, enriching excludeOnUpgradeQuery bool: { - filter: [ + must_not: [ { - bool: { - must_not: [ - { - term: { - type: 'unused-fleet-agent-events', - }, - }, - ], + term: { + type: 'unused-fleet-agent-events', }, }, { - bool: { must: { term: { fieldA: 'abc' } } }, + term: { + fieldA: 'abc', + }, }, ], }, diff --git a/src/core/server/saved_objects/migrations/model/model.ts b/src/core/server/saved_objects/migrations/model/model.ts index accff9553c808..b552b9cbb049a 100644 --- a/src/core/server/saved_objects/migrations/model/model.ts +++ b/src/core/server/saved_objects/migrations/model/model.ts @@ -8,10 +8,9 @@ import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { AliasAction, isLeftTypeof } from '../actions'; -import { AllActionStates, State } from '../state'; +import { type AliasAction, isTypeof } from '../actions'; +import type { AllActionStates, State } from '../state'; import type { ResponseType } from '../next'; import { disableUnknownTypeMappingFields } from '../core'; import { @@ -25,9 +24,13 @@ import { extractTransformFailuresReason, extractUnknownDocFailureReason, fatalReasonDocumentExceedsMaxBatchSizeBytes, + extractDiscardedUnknownDocs, } from './extract_errors'; import type { ExcludeRetryableEsError } from './types'; import { + addExcludedTypesToBoolQuery, + addMustClausesToBoolQuery, + addMustNotClausesToBoolQuery, getAliases, indexBelongsToLaterVersion, indexVersion, @@ -36,6 +39,7 @@ import { throwBadResponse, } from './helpers'; import { createBatches } from './create_batches'; +import type { MigrationLog } from '../types'; export const FATAL_REASON_REQUEST_ENTITY_TOO_LARGE = `While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Ensure that the Kibana configuration option 'migrations.maxBatchSizeBytes' is set to a value that is lower than or equal to the Elasticsearch 'http.max_content_length' configuration option.`; const CLUSTER_SHARD_LIMIT_EXCEEDED_REASON = `[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources.`; @@ -53,7 +57,7 @@ export const model = (currentState: State, resW: ResponseType): // Handle retryable_es_client_errors. Other left values need to be handled // by the control state specific code below. if (Either.isLeft(resW)) { - if (isLeftTypeof(resW.left, 'retryable_es_client_error')) { + if (isTypeof(resW.left, 'retryable_es_client_error')) { // Retry the same step after an exponentially increasing delay. return delayRetryState(stateP, resW.left.message, stateP.retryAttempts); } @@ -67,7 +71,7 @@ export const model = (currentState: State, resW: ResponseType): if (Either.isLeft(res)) { const left = res.left; - if (isLeftTypeof(left, 'incompatible_cluster_routing_allocation')) { + if (isTypeof(left, 'incompatible_cluster_routing_allocation')) { const retryErrorMessage = `[${left.type}] Incompatible Elasticsearch cluster settings detected. Remove the persistent and transient Elasticsearch cluster setting 'cluster.routing.allocation.enable' or set it to a value of 'all' to allow migrations to proceed. Refer to ${stateP.migrationDocLinks.routingAllocationDisabled} for more information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); } else { @@ -209,7 +213,7 @@ export const model = (currentState: State, resW: ResponseType): // If the write block failed because the index doesn't exist, it means // another instance already completed the legacy pre-migration. Proceed // to the next step. - if (isLeftTypeof(res.left, 'index_not_found_exception')) { + if (isTypeof(res.left, 'index_not_found_exception')) { return { ...stateP, controlState: 'LEGACY_CREATE_REINDEX_TARGET' }; } else { // @ts-expect-error TS doesn't correctly narrow this type to never @@ -222,7 +226,7 @@ export const model = (currentState: State, resW: ResponseType): const res = resW as ExcludeRetryableEsError>; if (Either.isLeft(res)) { const left = res.left; - if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + if (isTypeof(left, 'index_not_yellow_timeout')) { // `index_not_yellow_timeout` for the LEGACY_CREATE_REINDEX_TARGET source index: // A yellow status timeout could theoretically be temporary for a busy cluster // that takes a long time to allocate the primary and we retry the action to see if @@ -231,7 +235,7 @@ export const model = (currentState: State, resW: ResponseType): // continue to timeout and eventually lead to a failed migration. const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); - } else if (isLeftTypeof(left, 'cluster_shard_limit_exceeded')) { + } else if (isTypeof(left, 'cluster_shard_limit_exceeded')) { return { ...stateP, controlState: 'FATAL', @@ -272,8 +276,8 @@ export const model = (currentState: State, resW: ResponseType): } else { const left = res.left; if ( - (isLeftTypeof(left, 'index_not_found_exception') && left.index === stateP.legacyIndex) || - isLeftTypeof(left, 'target_index_had_write_block') + (isTypeof(left, 'index_not_found_exception') && left.index === stateP.legacyIndex) || + isTypeof(left, 'target_index_had_write_block') ) { // index_not_found_exception for the LEGACY_REINDEX source index: // another instance already complete the LEGACY_DELETE step. @@ -286,15 +290,15 @@ export const model = (currentState: State, resW: ResponseType): // step. However, by not skipping ahead we limit branches in the // control state progression and simplify the implementation. return { ...stateP, controlState: 'LEGACY_DELETE' }; - } else if (isLeftTypeof(left, 'wait_for_task_completion_timeout')) { + } else if (isTypeof(left, 'wait_for_task_completion_timeout')) { // After waiting for the specified timeout, the task has not yet // completed. Retry this step to see if the task has completed after an // exponential delay. We will basically keep polling forever until the // Elasticsearch task succeeds or fails. return delayRetryState(stateP, left.message, Number.MAX_SAFE_INTEGER); } else if ( - isLeftTypeof(left, 'index_not_found_exception') || - isLeftTypeof(left, 'incompatible_mapping_exception') + isTypeof(left, 'index_not_found_exception') || + isTypeof(left, 'incompatible_mapping_exception') ) { // We don't handle the following errors as the algorithm will never // run into these during the LEGACY_REINDEX_WAIT_FOR_TASK step: @@ -312,8 +316,8 @@ export const model = (currentState: State, resW: ResponseType): } else if (Either.isLeft(res)) { const left = res.left; if ( - isLeftTypeof(left, 'remove_index_not_a_concrete_index') || - (isLeftTypeof(left, 'index_not_found_exception') && left.index === stateP.legacyIndex) + isTypeof(left, 'remove_index_not_a_concrete_index') || + (isTypeof(left, 'index_not_found_exception') && left.index === stateP.legacyIndex) ) { // index_not_found_exception, another Kibana instance already // deleted the legacy index @@ -327,8 +331,8 @@ export const model = (currentState: State, resW: ResponseType): // control state progression and simplify the implementation. return { ...stateP, controlState: 'SET_SOURCE_WRITE_BLOCK' }; } else if ( - isLeftTypeof(left, 'index_not_found_exception') || - isLeftTypeof(left, 'alias_not_found_exception') + isTypeof(left, 'index_not_found_exception') || + isTypeof(left, 'alias_not_found_exception') ) { // We don't handle the following errors as the migration algorithm // will never cause them to occur: @@ -351,7 +355,7 @@ export const model = (currentState: State, resW: ResponseType): }; } else if (Either.isLeft(res)) { const left = res.left; - if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + if (isTypeof(left, 'index_not_yellow_timeout')) { // A yellow status timeout could theoretically be temporary for a busy cluster // that takes a long time to allocate the primary and we retry the action to see if // we get a response. @@ -368,36 +372,61 @@ export const model = (currentState: State, resW: ResponseType): } else if (stateP.controlState === 'CHECK_UNKNOWN_DOCUMENTS') { const res = resW as ExcludeRetryableEsError>; - if (Either.isRight(res)) { - const source = stateP.sourceIndex; - const target = stateP.versionIndex; - return { - ...stateP, - controlState: 'SET_SOURCE_WRITE_BLOCK', - sourceIndex: source, - targetIndex: target, - targetIndexMappings: disableUnknownTypeMappingFields( - stateP.targetIndexMappings, - stateP.sourceIndexMappings - ), - versionIndexReadyActions: Option.some([ - { remove: { index: source.value, alias: stateP.currentAlias, must_exist: true } }, - { add: { index: target, alias: stateP.currentAlias } }, - { add: { index: target, alias: stateP.versionAlias } }, - { remove_index: { index: stateP.tempIndex } }, - ]), - }; - } else { - if (isLeftTypeof(res.left, 'unknown_docs_found')) { + let logs: MigrationLog[] = stateP.logs; + let excludeOnUpgradeQuery = stateP.excludeOnUpgradeQuery; + + if (isTypeof(res.right, 'unknown_docs_found')) { + if (!stateP.discardUnknownObjects) { return { ...stateP, controlState: 'FATAL', - reason: extractUnknownDocFailureReason(res.left.unknownDocs, stateP.sourceIndex.value), + reason: extractUnknownDocFailureReason( + stateP.migrationDocLinks.resolveMigrationFailures, + res.right.unknownDocs + ), }; - } else { - return throwBadResponse(stateP, res.left); } + + // at this point, users have configured kibana to discard unknown objects + // thus, we can ignore unknown documents and proceed with the migration + logs = [ + ...stateP.logs, + { level: 'warning', message: extractDiscardedUnknownDocs(res.right.unknownDocs) }, + ]; + + const unknownTypes = [...new Set(res.right.unknownDocs.map(({ type }) => type))]; + + excludeOnUpgradeQuery = addExcludedTypesToBoolQuery( + unknownTypes, + stateP.excludeOnUpgradeQuery?.bool + ); + + excludeOnUpgradeQuery = addMustClausesToBoolQuery( + [{ exists: { field: 'type' } }], + excludeOnUpgradeQuery.bool + ); } + + const source = stateP.sourceIndex; + const target = stateP.versionIndex; + return { + ...stateP, + controlState: 'SET_SOURCE_WRITE_BLOCK', + logs, + excludeOnUpgradeQuery, + sourceIndex: source, + targetIndex: target, + targetIndexMappings: disableUnknownTypeMappingFields( + stateP.targetIndexMappings, + stateP.sourceIndexMappings + ), + versionIndexReadyActions: Option.some([ + { remove: { index: source.value, alias: stateP.currentAlias, must_exist: true } }, + { add: { index: target, alias: stateP.currentAlias } }, + { add: { index: target, alias: stateP.versionAlias } }, + { remove_index: { index: stateP.tempIndex } }, + ]), + }; } else if (stateP.controlState === 'SET_SOURCE_WRITE_BLOCK') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { @@ -406,7 +435,7 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'CALCULATE_EXCLUDE_FILTERS', }; - } else if (isLeftTypeof(res.left, 'index_not_found_exception')) { + } else if (isTypeof(res.left, 'index_not_found_exception')) { // We don't handle the following errors as the migration algorithm // will never cause them to occur: // - index_not_found_exception @@ -418,16 +447,15 @@ export const model = (currentState: State, resW: ResponseType): const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { - const unusedTypesQuery: estypes.QueryDslQueryContainer = { - bool: { - filter: [stateP.unusedTypesQuery, res.right.excludeFilter], - }, - }; + const excludeOnUpgradeQuery = addMustNotClausesToBoolQuery( + res.right.mustNotClauses, + stateP.excludeOnUpgradeQuery?.bool + ); return { ...stateP, controlState: 'CREATE_REINDEX_TEMP', - unusedTypesQuery, + excludeOnUpgradeQuery, logs: [ ...stateP.logs, ...Object.entries(res.right.errorsByType).map(([soType, error]) => ({ @@ -445,7 +473,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT' }; } else if (Either.isLeft(res)) { const left = res.left; - if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + if (isTypeof(left, 'index_not_yellow_timeout')) { // `index_not_yellow_timeout` for the CREATE_REINDEX_TEMP target temp index: // The index status did not go yellow within the specified timeout period. // A yellow status timeout could theoretically be temporary for a busy cluster. @@ -454,7 +482,7 @@ export const model = (currentState: State, resW: ResponseType): // continue to timeout and eventually lead to a failed migration. const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); - } else if (isLeftTypeof(left, 'cluster_shard_limit_exceeded')) { + } else if (isTypeof(left, 'cluster_shard_limit_exceeded')) { return { ...stateP, controlState: 'FATAL', @@ -588,7 +616,7 @@ export const model = (currentState: State, resW: ResponseType): } else { // we have failures from the current batch of documents and add them to the lists const left = res.left; - if (isLeftTypeof(left, 'documents_transform_failed')) { + if (isTypeof(left, 'documents_transform_failed')) { return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_READ', @@ -621,8 +649,8 @@ export const model = (currentState: State, resW: ResponseType): } } else { if ( - isLeftTypeof(res.left, 'target_index_had_write_block') || - isLeftTypeof(res.left, 'index_not_found_exception') + isTypeof(res.left, 'target_index_had_write_block') || + isTypeof(res.left, 'index_not_found_exception') ) { // When the temp index has a write block or has been deleted another // instance already completed this step. Close the PIT search and carry @@ -631,7 +659,7 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', }; - } else if (isLeftTypeof(res.left, 'request_entity_too_large_exception')) { + } else if (isTypeof(res.left, 'request_entity_too_large_exception')) { return { ...stateP, controlState: 'FATAL', @@ -649,7 +677,7 @@ export const model = (currentState: State, resW: ResponseType): }; } else { const left = res.left; - if (isLeftTypeof(left, 'index_not_found_exception')) { + if (isTypeof(left, 'index_not_found_exception')) { // index_not_found_exception: // another instance completed the MARK_VERSION_INDEX_READY and // removed the temp index. @@ -673,7 +701,7 @@ export const model = (currentState: State, resW: ResponseType): }; } else { const left = res.left; - if (isLeftTypeof(left, 'index_not_found_exception')) { + if (isTypeof(left, 'index_not_found_exception')) { // index_not_found_exception means another instance already completed // the MARK_VERSION_INDEX_READY step and removed the temp index // We still perform the REFRESH_TARGET, OUTDATED_DOCUMENTS_* and @@ -683,7 +711,7 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'REFRESH_TARGET', }; - } else if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + } else if (isTypeof(left, 'index_not_yellow_timeout')) { // `index_not_yellow_timeout` for the CLONE_TEMP_TO_TARGET source -> target index: // The target index status did not go yellow within the specified timeout period. // The cluster could just be busy and we retry the action. @@ -695,7 +723,7 @@ export const model = (currentState: State, resW: ResponseType): // continue to timeout and eventually lead to a failed migration. const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); - } else if (isLeftTypeof(left, 'cluster_shard_limit_exceeded')) { + } else if (isTypeof(left, 'cluster_shard_limit_exceeded')) { return { ...stateP, controlState: 'FATAL', @@ -817,7 +845,7 @@ export const model = (currentState: State, resW: ResponseType): }; } } else { - if (isLeftTypeof(res.left, 'documents_transform_failed')) { + if (isTypeof(res.left, 'documents_transform_failed')) { // continue to build up any more transformation errors before failing the migration. return { ...stateP, @@ -849,15 +877,15 @@ export const model = (currentState: State, resW: ResponseType): hasTransformedDocs: true, }; } else { - if (isLeftTypeof(res.left, 'request_entity_too_large_exception')) { + if (isTypeof(res.left, 'request_entity_too_large_exception')) { return { ...stateP, controlState: 'FATAL', reason: FATAL_REASON_REQUEST_ENTITY_TOO_LARGE, }; } else if ( - isLeftTypeof(res.left, 'target_index_had_write_block') || - isLeftTypeof(res.left, 'index_not_found_exception') + isTypeof(res.left, 'target_index_had_write_block') || + isTypeof(res.left, 'index_not_found_exception') ) { // we fail on these errors since the target index will never get // deleted and should only have a write block if a newer version of @@ -929,7 +957,7 @@ export const model = (currentState: State, resW: ResponseType): } } else { const left = res.left; - if (isLeftTypeof(left, 'wait_for_task_completion_timeout')) { + if (isTypeof(left, 'wait_for_task_completion_timeout')) { // After waiting for the specified timeout, the task has not yet // completed. Retry this step to see if the task has completed after an // exponential delay. We will basically keep polling forever until the @@ -948,7 +976,7 @@ export const model = (currentState: State, resW: ResponseType): }; } else if (Either.isLeft(res)) { const left = res.left; - if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + if (isTypeof(left, 'index_not_yellow_timeout')) { // `index_not_yellow_timeout` for the CREATE_NEW_TARGET target index: // The cluster might just be busy and we retry the action for a set number of times. // If the cluster hit the low watermark for disk usage the action will continue to timeout. @@ -956,7 +984,7 @@ export const model = (currentState: State, resW: ResponseType): // continue to timeout and eventually lead to a failed migration. const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); - } else if (isLeftTypeof(left, 'cluster_shard_limit_exceeded')) { + } else if (isTypeof(left, 'cluster_shard_limit_exceeded')) { return { ...stateP, controlState: 'FATAL', @@ -977,13 +1005,13 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'DONE' }; } else { const left = res.left; - if (isLeftTypeof(left, 'alias_not_found_exception')) { + if (isTypeof(left, 'alias_not_found_exception')) { // the versionIndexReadyActions checks that the currentAlias is still // pointing to the source index. If this fails with an // alias_not_found_exception another instance has completed a // migration from the same source. return { ...stateP, controlState: 'MARK_VERSION_INDEX_READY_CONFLICT' }; - } else if (isLeftTypeof(left, 'index_not_found_exception')) { + } else if (isTypeof(left, 'index_not_found_exception')) { if (left.index === stateP.tempIndex) { // another instance has already completed the migration and deleted // the temporary index @@ -994,7 +1022,7 @@ export const model = (currentState: State, resW: ResponseType): // index handled above. throwBadResponse(stateP, left as never); } - } else if (isLeftTypeof(left, 'remove_index_not_a_concrete_index')) { + } else if (isTypeof(left, 'remove_index_not_a_concrete_index')) { // We don't handle this error as the migration algorithm will never // cause it to occur (this error is only relevant to the LEGACY_DELETE // step). diff --git a/src/core/server/saved_objects/migrations/next.ts b/src/core/server/saved_objects/migrations/next.ts index 24a4204c3009e..e50331d3b7658 100644 --- a/src/core/server/saved_objects/migrations/next.ts +++ b/src/core/server/saved_objects/migrations/next.ts @@ -40,9 +40,9 @@ import type { CheckUnknownDocumentsState, CalculateExcludeFiltersState, } from './state'; -import { TransformRawDocs } from './types'; +import type { TransformRawDocs } from './types'; import * as Actions from './actions'; -import { ElasticsearchClient } from '../../elasticsearch'; +import type { ElasticsearchClient } from '../../elasticsearch'; type ActionMap = ReturnType; @@ -66,7 +66,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra Actions.checkForUnknownDocs({ client, indexName: state.sourceIndex.value, - unusedTypesQuery: state.unusedTypesQuery, + excludeOnUpgradeQuery: state.excludeOnUpgradeQuery, knownTypes: state.knownTypes, }), SET_SOURCE_WRITE_BLOCK: (state: SetSourceWriteBlockState) => @@ -98,7 +98,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra * are no longer used. These saved objects will still be kept in the outdated * index for backup purposes, but won't be available in the upgraded index. */ - query: state.unusedTypesQuery, + query: state.excludeOnUpgradeQuery, batchSize: state.batchSize, searchAfter: state.lastHitSortValue, }), @@ -187,7 +187,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra targetIndex: state.sourceIndex.value, reindexScript: state.preMigrationScript, requireAlias: false, - unusedTypesQuery: state.unusedTypesQuery, + excludeOnUpgradeQuery: state.excludeOnUpgradeQuery, }), LEGACY_REINDEX_WAIT_FOR_TASK: (state: LegacyReindexWaitForTaskState) => Actions.waitForReindexTask({ client, taskId: state.legacyReindexTaskId, timeout: '60s' }), diff --git a/src/core/server/saved_objects/migrations/run_resilient_migrator.ts b/src/core/server/saved_objects/migrations/run_resilient_migrator.ts index 6e5766c9f5dcf..194496b826b0e 100644 --- a/src/core/server/saved_objects/migrations/run_resilient_migrator.ts +++ b/src/core/server/saved_objects/migrations/run_resilient_migrator.ts @@ -59,6 +59,7 @@ export async function runResilientMigrator({ migrationsConfig, typeRegistry, docLinks, + logger, }); return migrationStateActionMachine({ initialState, diff --git a/src/core/server/saved_objects/migrations/state.ts b/src/core/server/saved_objects/migrations/state.ts index dee6839d6b902..3dac96987eb92 100644 --- a/src/core/server/saved_objects/migrations/state.ts +++ b/src/core/server/saved_objects/migrations/state.ts @@ -7,15 +7,15 @@ */ import * as Option from 'fp-ts/lib/Option'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { DocLinks } from '@kbn/doc-links'; -import { ControlState } from './state_action_machine'; -import { AliasAction } from './actions'; -import { IndexMapping } from '../mappings'; -import { SavedObjectsRawDoc } from '..'; -import { TransformErrorObjects } from './core'; -import { SavedObjectTypeExcludeFromUpgradeFilterHook } from '../types'; -import { MigrationLog, Progress } from './types'; +import type { ControlState } from './state_action_machine'; +import type { AliasAction } from './actions'; +import type { IndexMapping } from '../mappings'; +import type { SavedObjectsRawDoc } from '..'; +import type { TransformErrorObjects } from './core'; +import type { SavedObjectTypeExcludeFromUpgradeFilterHook } from '../types'; +import type { MigrationLog, Progress } from './types'; export interface BaseState extends ControlState { /** The first part of the index name such as `.kibana` or `.kibana_task_manager` */ @@ -39,7 +39,7 @@ export interface BaseState extends ControlState { readonly tempIndexMappings: IndexMapping; /** Script to apply to a legacy index before it can be used as a migration source */ readonly preMigrationScript: Option.Option; - readonly outdatedDocumentsQuery: estypes.QueryDslQueryContainer; + readonly outdatedDocumentsQuery: QueryDslQueryContainer; readonly retryCount: number; readonly retryDelay: number; /** @@ -87,6 +87,14 @@ export interface BaseState extends ControlState { */ readonly maxBatchSizeBytes: number; readonly logs: MigrationLog[]; + /** + * If saved objects exist which have an unknown type they will cause + * the migration to fail. If this flag is set to `true`, kibana will + * discard the unknown objects and proceed with the migration. + * This can happen, for instance, if a plugin that had registered some + * saved objects is disabled. + */ + readonly discardUnknownObjects: boolean; /** * The current alias e.g. `.kibana` which always points to the latest * version index @@ -107,11 +115,16 @@ export interface BaseState extends ControlState { */ readonly tempIndex: string; /** - * When reindexing we use a source query to exclude saved objects types which - * are no longer used. These saved objects will still be kept in the outdated + * When upgrading to a more recent kibana version, some saved object types + * might be conflicting or no longer used. + * When reindexing, we use a source query to exclude types which are: + * - no longer used + * - unknown (e.g. belonging to plugins that have been disabled) + * - explicitly excluded from upgrades by plugin developers + * These saved objects will still be kept in the outdated * index for backup purposes, but won't be available in the upgraded index. */ - readonly unusedTypesQuery: estypes.QueryDslQueryContainer; + readonly excludeOnUpgradeQuery: QueryDslQueryContainer; /** * The list of known SO types that are registered. */ @@ -146,7 +159,7 @@ export interface PostInitState extends BaseState { /** The target index is the index to which the migration writes */ readonly targetIndex: string; readonly versionIndexReadyActions: Option.Option; - readonly outdatedDocumentsQuery: estypes.QueryDslQueryContainer; + readonly outdatedDocumentsQuery: QueryDslQueryContainer; } export interface DoneState extends PostInitState { diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 4a35ea116e8ee..f3b73897fbf1b 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -6,12 +6,19 @@ * Side Public License, v 1. */ +import { valid } from 'semver'; import { schema, TypeOf } from '@kbn/config-schema'; import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal'; const migrationSchema = schema.object({ batchSize: schema.number({ defaultValue: 1_000 }), maxBatchSizeBytes: schema.byteSize({ defaultValue: '100mb' }), // 100mb is the default http.max_content_length Elasticsearch config value + discardUnknownObjects: schema.maybe( + schema.string({ + validate: (value: string) => + valid(value) ? undefined : 'The value is not a valid semantic version', + }) + ), scrollDuration: schema.string({ defaultValue: '15m' }), pollInterval: schema.number({ defaultValue: 1_500 }), skip: schema.boolean({ defaultValue: false }), diff --git a/src/core/server/saved_objects/service/lib/find_shared_origin_objects.test.ts b/src/core/server/saved_objects/service/lib/find_shared_origin_objects.test.ts index c8e0796dea18e..ed6ecf7b33d61 100644 --- a/src/core/server/saved_objects/service/lib/find_shared_origin_objects.test.ts +++ b/src/core/server/saved_objects/service/lib/find_shared_origin_objects.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { CreatePointInTimeFinderFn, PointInTimeFinder } from './point_in_time_finder'; import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock'; diff --git a/src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.test.ts b/src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.test.ts index f0399f4b54aa0..f171b0651ffb4 100644 --- a/src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.test.ts +++ b/src/core/server/saved_objects/service/lib/legacy_url_aliases/find_legacy_url_aliases.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../../object_types'; import type { CreatePointInTimeFinderFn, PointInTimeFinder } from '../point_in_time_finder'; diff --git a/src/core/server/saved_objects/service/lib/preflight_check_for_create.test.ts b/src/core/server/saved_objects/service/lib/preflight_check_for_create.test.ts index 8d7cfd25ac885..b7c61bc6f7711 100644 --- a/src/core/server/saved_objects/service/lib/preflight_check_for_create.test.ts +++ b/src/core/server/saved_objects/service/lib/preflight_check_for_create.test.ts @@ -11,7 +11,7 @@ import { mockRawDocExistsInNamespaces, } from './preflight_check_for_create.test.mock'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { ElasticsearchClient } from '../../../elasticsearch'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a05d25bb37ecb..b65d335783d43 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -25,6 +25,7 @@ import { ConfigService } from '@kbn/config'; import { ContextProviderOpts } from '@kbn/analytics-client'; import { CoreId } from '@kbn/core-base-common-internal'; import { DetailedPeerCertificate } from 'tls'; +import { DiscoveredPlugin } from '@kbn/core-base-common'; import type { DocLinks } from '@kbn/doc-links'; import { Duration } from 'moment'; import { Duration as Duration_2 } from 'moment-timezone'; @@ -56,6 +57,7 @@ import { PathConfigType } from '@kbn/utils'; import { PeerCertificate } from 'tls'; import { PluginName } from '@kbn/core-base-common'; import { PluginOpaqueId } from '@kbn/core-base-common'; +import { PluginType } from '@kbn/core-base-common'; import { Readable } from 'stream'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Request as Request_2 } from '@hapi/hapi'; @@ -922,16 +924,7 @@ export interface DeprecationsServiceSetup { // @public export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; -// @public -export interface DiscoveredPlugin { - readonly configPath: ConfigPath; - readonly enabledOnAnonymousPages?: boolean; - readonly id: PluginName; - readonly optionalPlugins: readonly PluginName[]; - readonly requiredBundles: readonly PluginName[]; - readonly requiredPlugins: readonly PluginName[]; - readonly type: PluginType; -} +export { DiscoveredPlugin } // @public (undocumented) export interface DocLinksServiceSetup { @@ -1816,11 +1809,7 @@ export interface PluginsServiceStart { contracts: Map; } -// @public (undocumented) -export enum PluginType { - preboot = "preboot", - standard = "standard" -} +export { PluginType } // @public (undocumented) export const pollEsNodesVersion: ({ internalClient, log, kibanaVersion, ignoreVersionMismatch, esVersionCheckInterval: healthCheckInterval, }: PollEsNodesVersionOptions) => Observable; @@ -3256,8 +3245,8 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/elasticsearch/client/types.ts:81:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts // src/core/server/http/router/response.ts:302:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:403:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:405:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:512:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:339:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:341:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:448:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts index 11dabaca71e13..11ff892b89bf1 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock.ts @@ -6,7 +6,17 @@ * Side Public License, v 1. */ -export const createOrUpgradeSavedConfigMock = jest.fn(); -jest.doMock('./create_or_upgrade_saved_config', () => ({ - createOrUpgradeSavedConfig: createOrUpgradeSavedConfigMock, +import type { TransformConfigFn } from '../saved_objects'; +import type { getUpgradeableConfig } from './get_upgradeable_config'; + +export const mockTransform = jest.fn() as jest.MockedFunction; +jest.mock('../saved_objects', () => ({ + transforms: [mockTransform], +})); + +export const mockGetUpgradeableConfig = jest.fn() as jest.MockedFunction< + typeof getUpgradeableConfig +>; +jest.mock('./get_upgradeable_config', () => ({ + getUpgradeableConfig: mockGetUpgradeableConfig, })); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 669849dcd8d9b..fe6aa83bb0296 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -6,29 +6,28 @@ * Side Public License, v 1. */ -import Chance from 'chance'; - -import { getUpgradeableConfigMock } from './get_upgradeable_config.test.mock'; +import { + mockTransform, + mockGetUpgradeableConfig, +} from './create_or_upgrade_saved_config.test.mock'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { savedObjectsClientMock } from '../../saved_objects/service/saved_objects_client.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; -const chance = new Chance(); describe('uiSettings/createOrUpgradeSavedConfig', function () { afterEach(() => jest.resetAllMocks()); const version = '4.0.1'; const prevVersion = '4.0.0'; - const buildNum = chance.integer({ min: 1000, max: 5000 }); + const buildNum = 1337; function setup() { const logger = loggingSystemMock.create(); - const getUpgradeableConfig = getUpgradeableConfigMock; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.create.mockImplementation( - async (type, attributes, options = {}) => + async (type, _, options = {}) => ({ type, id: options.id, @@ -46,8 +45,8 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { ...options, }); - expect(getUpgradeableConfigMock).toHaveBeenCalledTimes(1); - expect(getUpgradeableConfig).toHaveBeenCalledWith({ savedObjectsClient, version }); + expect(mockGetUpgradeableConfig).toHaveBeenCalledTimes(1); + expect(mockGetUpgradeableConfig).toHaveBeenCalledWith({ savedObjectsClient, version }); return resp; } @@ -58,7 +57,6 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { run, version, savedObjectsClient, - getUpgradeableConfig, }; } @@ -83,25 +81,21 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { describe('something is upgradeable', () => { it('should merge upgraded attributes with current build number in new config', async () => { - const { run, getUpgradeableConfig, savedObjectsClient } = setup(); + const { run, savedObjectsClient } = setup(); const savedAttributes = { buildNum: buildNum - 100, - [chance.word()]: chance.sentence(), - [chance.word()]: chance.sentence(), - [chance.word()]: chance.sentence(), + defaultIndex: 'some-index', }; - getUpgradeableConfig.mockResolvedValue({ + mockGetUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: savedAttributes, - type: '', - references: [], }); await run(); - expect(getUpgradeableConfig).toHaveBeenCalledTimes(1); + expect(mockGetUpgradeableConfig).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create).toHaveBeenCalledWith( 'config', @@ -115,14 +109,42 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { ); }); + it('should prefer transformed attributes when merging', async () => { + const { run, savedObjectsClient } = setup(); + mockGetUpgradeableConfig.mockResolvedValue({ + id: prevVersion, + attributes: { + buildNum: buildNum - 100, + defaultIndex: 'some-index', + }, + }); + mockTransform.mockResolvedValue({ + defaultIndex: 'another-index', + isDefaultIndexMigrated: true, + }); + + await run(); + + expect(mockGetUpgradeableConfig).toHaveBeenCalledTimes(1); + expect(mockTransform).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'config', + { + buildNum, + defaultIndex: 'another-index', + isDefaultIndexMigrated: true, + }, + { id: version } + ); + }); + it('should log a message for upgrades', async () => { - const { getUpgradeableConfig, logger, run } = setup(); + const { logger, run } = setup(); - getUpgradeableConfig.mockResolvedValue({ + mockGetUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: { buildNum: buildNum - 100 }, - type: '', - references: [], }); await run(); @@ -144,13 +166,11 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { }); it('does not log when upgrade fails', async () => { - const { getUpgradeableConfig, logger, run, savedObjectsClient } = setup(); + const { logger, run, savedObjectsClient } = setup(); - getUpgradeableConfig.mockResolvedValue({ + mockGetUpgradeableConfig.mockResolvedValue({ id: prevVersion, attributes: { buildNum: buildNum - 100 }, - type: '', - references: [], }); savedObjectsClient.create.mockRejectedValue(new Error('foo')); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index d55a6537957cb..247a3cce974f4 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -9,10 +9,12 @@ import { defaults } from 'lodash'; import type { Logger, LogMeta } from '@kbn/logging'; +import { asyncForEach } from '@kbn/std'; import { SavedObjectsClientContract } from '../../saved_objects/types'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { getUpgradeableConfig } from './get_upgradeable_config'; +import { transforms } from '../saved_objects'; interface ConfigLogMeta extends LogMeta { kibana: { @@ -39,10 +41,22 @@ export async function createOrUpgradeSavedConfig( version, }); + let transformDefaults = {}; + await asyncForEach(transforms, async (transformFn) => { + const result = await transformFn({ + savedObjectsClient, + configAttributes: upgradeableConfig?.attributes, + }); + transformDefaults = { ...transformDefaults, ...result }; + }); + // default to the attributes of the upgradeableConfig if available const attributes = defaults( - { buildNum }, - upgradeableConfig ? (upgradeableConfig.attributes as any) : {} + { + buildNum, + ...transformDefaults, // Any defaults that should be applied from transforms + }, + upgradeableConfig?.attributes ); try { diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts index 320fa9056fcaf..34ef0c5b4f2e1 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.test.ts @@ -8,69 +8,70 @@ import { getUpgradeableConfig } from './get_upgradeable_config'; import { savedObjectsClientMock } from '../../saved_objects/service/saved_objects_client.mock'; +import { SavedObjectsFindResponse } from '../../saved_objects'; describe('getUpgradeableConfig', () => { it('finds saved objects with type "config"', async () => { const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ - saved_objects: [{ id: '7.5.0' }], - } as any); + saved_objects: [{ id: '7.5.0', attributes: 'foo' }], + } as SavedObjectsFindResponse); await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); expect(savedObjectsClient.find.mock.calls[0][0].type).toBe('config'); }); it('finds saved config with version < than Kibana version', async () => { - const savedConfig = { id: '7.4.0' }; + const savedConfig = { id: '7.4.0', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [savedConfig], - } as any); + } as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(savedConfig); + expect(result).toEqual(savedConfig); }); it('finds saved config with RC version === Kibana version', async () => { - const savedConfig = { id: '7.5.0-rc1' }; + const savedConfig = { id: '7.5.0-rc1', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [savedConfig], - } as any); + } as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(savedConfig); + expect(result).toEqual(savedConfig); }); it('does not find saved config with version === Kibana version', async () => { - const savedConfig = { id: '7.5.0' }; + const savedConfig = { id: '7.5.0', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [savedConfig], - } as any); + } as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(undefined); + expect(result).toBe(null); }); it('does not find saved config with version > Kibana version', async () => { - const savedConfig = { id: '7.6.0' }; + const savedConfig = { id: '7.6.0', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [savedConfig], - } as any); + } as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(undefined); + expect(result).toBe(null); }); it('handles empty config', async () => { const savedObjectsClient = savedObjectsClientMock.create(); savedObjectsClient.find.mockResolvedValue({ saved_objects: [], - } as any); + } as unknown as SavedObjectsFindResponse); const result = await getUpgradeableConfig({ savedObjectsClient, version: '7.5.0' }); - expect(result).toBe(undefined); + expect(result).toBe(null); }); }); diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts index 493efa518f22b..6c8e8cbe9ac3a 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts @@ -7,8 +7,18 @@ */ import { SavedObjectsClientContract } from '../../saved_objects/types'; +import type { ConfigAttributes } from '../saved_objects'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; +/** + * This contains a subset of `config` object attributes that are relevant for upgrading it using transform functions. + * It is a superset of all the attributes needed for all of the transform functions defined in `transforms.ts`. + */ +export interface UpgradeableConfigAttributes extends ConfigAttributes { + defaultIndex?: string; + isDefaultIndexMigrated?: boolean; +} + /** * Find the most recent SavedConfig that is upgradeable to the specified version * @param {Object} options @@ -24,14 +34,21 @@ export async function getUpgradeableConfig({ version: string; }) { // attempt to find a config we can upgrade - const { saved_objects: savedConfigs } = await savedObjectsClient.find({ - type: 'config', - page: 1, - perPage: 1000, - sortField: 'buildNum', - sortOrder: 'desc', - }); + const { saved_objects: savedConfigs } = + await savedObjectsClient.find({ + type: 'config', + page: 1, + perPage: 1000, + sortField: 'buildNum', + sortOrder: 'desc', + }); // try to find a config that we can upgrade - return savedConfigs.find((savedConfig) => isConfigVersionUpgradeable(savedConfig.id, version)); + const findResult = savedConfigs.find((savedConfig) => + isConfigVersionUpgradeable(savedConfig.id, version) + ); + if (findResult) { + return { id: findResult.id, attributes: findResult.attributes }; + } + return null; } diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/index.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/index.ts index 3559c8555803a..e5f8f895e8631 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/index.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/index.ts @@ -7,3 +7,4 @@ */ export { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; +export type { UpgradeableConfigAttributes } from './get_upgradeable_config'; diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index 1da8bad330c31..44efc558e1029 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -90,6 +90,9 @@ describe('createOrUpgradeSavedConfig()', () => { // 5.4.0-SNAPSHOT and @@version were ignored so we only have the // attributes from 5.4.0-rc1, even though the other build nums are greater '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); // add the 5.4.0 flag to the 5.4.0 savedConfig @@ -115,6 +118,9 @@ describe('createOrUpgradeSavedConfig()', () => { // should also include properties from 5.4.0 and 5.4.0-rc1 '5.4.0': true, '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); // add the 5.4.1 flag to the 5.4.1 savedConfig @@ -141,6 +147,9 @@ describe('createOrUpgradeSavedConfig()', () => { '5.4.1': true, '5.4.0': true, '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); // tag the 7.0.0-rc1 doc @@ -168,6 +177,9 @@ describe('createOrUpgradeSavedConfig()', () => { '5.4.1': true, '5.4.0': true, '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); // tag the 7.0.0 doc @@ -194,6 +206,9 @@ describe('createOrUpgradeSavedConfig()', () => { '5.4.1': true, '5.4.0': true, '5.4.0-rc1': true, + + // Should have the transform(s) applied + isDefaultIndexMigrated: true, }); }, 30000); }); diff --git a/src/core/server/ui_settings/saved_objects/index.ts b/src/core/server/ui_settings/saved_objects/index.ts index eb8e7cfcc2a1b..92ef9961bb908 100644 --- a/src/core/server/ui_settings/saved_objects/index.ts +++ b/src/core/server/ui_settings/saved_objects/index.ts @@ -6,4 +6,7 @@ * Side Public License, v 1. */ +export type { ConfigAttributes } from './ui_settings'; export { uiSettingsType } from './ui_settings'; +export type { TransformConfigFn } from './transforms'; +export { transforms } from './transforms'; diff --git a/src/core/server/ui_settings/saved_objects/transforms.test.ts b/src/core/server/ui_settings/saved_objects/transforms.test.ts new file mode 100644 index 0000000000000..afcd525673aa0 --- /dev/null +++ b/src/core/server/ui_settings/saved_objects/transforms.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { savedObjectsClientMock } from '../../mocks'; +import { SavedObjectsErrorHelpers } from '../../saved_objects'; +import { SavedObject } from '../../types'; +import type { UpgradeableConfigAttributes } from '../create_or_upgrade_saved_config'; +import { transformDefaultIndex } from './transforms'; + +/** + * Test each transform function individually, not the entire exported `transforms` array. + */ +describe('#transformDefaultIndex', () => { + const savedObjectsClient = savedObjectsClientMock.create(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return early if the config object has already been transformed', async () => { + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { isDefaultIndexMigrated: true } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).not.toHaveBeenCalled(); + expect(result).toEqual(null); // This is the only time we expect a null result + }); + + it('should return early if configAttributes is undefined', async () => { + const result = await transformDefaultIndex({ savedObjectsClient, configAttributes: undefined }); + + expect(savedObjectsClient.resolve).not.toHaveBeenCalled(); + expect(result).toEqual({ isDefaultIndexMigrated: true }); + }); + + it('should return early if the defaultIndex attribute is undefined', async () => { + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: undefined } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).not.toHaveBeenCalled(); + expect(result).toEqual({ isDefaultIndexMigrated: true }); + }); + + describe('should resolve the data view for the defaultIndex and return the result according to the outcome', () => { + it('outcome: exactMatch', async () => { + savedObjectsClient.resolve.mockResolvedValue({ + outcome: 'exactMatch', + alias_target_id: 'another-index', // This wouldn't realistically be set if the outcome is exactMatch, but we're including it in this test to assert that the returned defaultIndex will be 'some-index' + saved_object: {} as SavedObject, // Doesn't matter + }); + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: 'some-index' } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.resolve).toHaveBeenCalledWith('index-pattern', 'some-index'); + expect(result).toEqual({ isDefaultIndexMigrated: true, defaultIndex: 'some-index' }); + }); + + for (const outcome of ['aliasMatch' as const, 'conflict' as const]) { + it(`outcome: ${outcome}`, async () => { + savedObjectsClient.resolve.mockResolvedValue({ + outcome, + alias_target_id: 'another-index', + saved_object: {} as SavedObject, // Doesn't matter + }); + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: 'some-index' } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.resolve).toHaveBeenCalledWith('index-pattern', 'some-index'); + expect(result).toEqual({ isDefaultIndexMigrated: true, defaultIndex: 'another-index' }); + }); + } + + it('returns the expected result if resolve fails with a Not Found error', async () => { + savedObjectsClient.resolve.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError('Oh no!') + ); + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: 'some-index' } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.resolve).toHaveBeenCalledWith('index-pattern', 'some-index'); + expect(result).toEqual({ isDefaultIndexMigrated: true, defaultIndex: 'some-index' }); + }); + + it('returns the expected result if resolve fails with another error', async () => { + savedObjectsClient.resolve.mockRejectedValue( + SavedObjectsErrorHelpers.createIndexAliasNotFoundError('Oh no!') + ); + const result = await transformDefaultIndex({ + savedObjectsClient, + configAttributes: { defaultIndex: 'some-index' } as UpgradeableConfigAttributes, // We don't care about the other attributes + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.resolve).toHaveBeenCalledWith('index-pattern', 'some-index'); + expect(result).toEqual({ isDefaultIndexMigrated: false, defaultIndex: 'some-index' }); + }); + }); +}); diff --git a/src/core/server/ui_settings/saved_objects/transforms.ts b/src/core/server/ui_settings/saved_objects/transforms.ts new file mode 100644 index 0000000000000..cabf14a2e6469 --- /dev/null +++ b/src/core/server/ui_settings/saved_objects/transforms.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { SavedObjectsErrorHelpers } from '../../saved_objects'; +import type { SavedObjectsClientContract } from '../../types'; +import type { UpgradeableConfigAttributes } from '../create_or_upgrade_saved_config'; + +/** + * The params needed to execute each transform function. + */ +interface TransformParams { + savedObjectsClient: SavedObjectsClientContract; + configAttributes: UpgradeableConfigAttributes | undefined; +} + +/** + * The resulting attributes that should be used when upgrading the config object. + * This should be a union of all transform function return types (A | B | C | ...). + */ +type TransformReturnType = TransformDefaultIndexReturnType; + +/** + * The return type for `transformDefaultIndex`. + * If this config object has already been upgraded, it returns `null` because it doesn't need to set different default attributes. + * Otherwise, it always sets a default for the `isDefaultIndexMigrated` attribute, and it optionally sets the `defaultIndex` attribute + * depending on the outcome. + */ +type TransformDefaultIndexReturnType = { + isDefaultIndexMigrated: boolean; + defaultIndex?: string; +} | null; + +export type TransformConfigFn = (params: TransformParams) => Promise; + +/** + * Any transforms that should be applied during `createOrUpgradeSavedConfig` need to be included in this array. + */ +export const transforms: TransformConfigFn[] = [transformDefaultIndex]; + +/** + * This optionally transforms the `defaultIndex` attribute of a config saved object. The `defaultIndex` attribute points to a data view ID, + * but those saved object IDs were regenerated in the 8.0 upgrade. That resulted in a bug where the `defaultIndex` would be broken in custom + * spaces. + * + * We are fixing this bug after the fact in 8.3, and we can't retroactively change a saved object that's already been migrated, so we use + * this transformation instead to ensure that the `defaultIndex` attribute is not broken. + * + * Note that what used to be called "index patterns" prior to 8.0 have been renamed to "data views", but the object type cannot be changed, + * so that type remains `index-pattern`. + * + * Note also that this function is only exported for unit testing. It is also included in the `transforms` export above, which is how it is + * applied during `createOrUpgradeSavedConfig`. + */ +export async function transformDefaultIndex( + params: TransformParams +): Promise { + const { savedObjectsClient, configAttributes } = params; + if (configAttributes?.isDefaultIndexMigrated) { + // This config object has already been migrated, return null because we don't need to set different defaults for the new config object. + return null; + } + if (!configAttributes?.defaultIndex) { + // If configAttributes is undefined (there's no config object being upgraded), OR if configAttributes is defined but the defaultIndex + // attribute is not set, set isDefaultIndexMigrated to true and return. This means there was no defaultIndex to upgrade, so we will just + // avoid attempting to transform this again in the future. + return { isDefaultIndexMigrated: true }; + } + + let defaultIndex = configAttributes.defaultIndex; // Retain the existing defaultIndex attribute in case we run into a resolve error + let isDefaultIndexMigrated: boolean; + try { + // The defaultIndex for this config object was created prior to 8.3, and it might refer to a data view ID that is no longer valid. + // We should try to resolve the data view and change the defaultIndex to the new ID, if necessary. + const resolvedDataView = await savedObjectsClient.resolve('index-pattern', defaultIndex); + if (resolvedDataView.outcome === 'aliasMatch' || resolvedDataView.outcome === 'conflict') { + // This resolved to an aliasMatch or conflict outcome; that means we should change the defaultIndex to the data view's new ID. + // Note, the alias_target_id field is guaranteed to exist iff the resolve outcome is aliasMatch or conflict. + defaultIndex = resolvedDataView.alias_target_id!; + } + isDefaultIndexMigrated = true; // Regardless of the resolve outcome, we now consider this defaultIndex attribute to be migrated + } catch (err) { + // If the defaultIndex is not found at all, it will throw a Not Found error and we should mark the defaultIndex attribute as upgraded. + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + isDefaultIndexMigrated = true; + } else { + // For any other error, explicitly set isDefaultIndexMigrated to false so we can try this upgrade again in the future. + isDefaultIndexMigrated = false; + } + } + return { isDefaultIndexMigrated, defaultIndex }; +} diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts index 9db1402dc2d26..184ec5d41987a 100644 --- a/src/core/server/ui_settings/saved_objects/ui_settings.ts +++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts @@ -9,6 +9,14 @@ import { SavedObjectsType } from '../../saved_objects'; import { migrations } from './migrations'; +/** + * The `config` object type contains many attributes that are defined by consumers. + */ +export interface ConfigAttributes { + buildNum: number; + [key: string]: unknown; +} + export const uiSettingsType: SavedObjectsType = { name: 'config', hidden: false, diff --git a/src/core/server/ui_settings/ui_settings_client.test.mock.ts b/src/core/server/ui_settings/ui_settings_client.test.mock.ts new file mode 100644 index 0000000000000..d2a96352b36ea --- /dev/null +++ b/src/core/server/ui_settings/ui_settings_client.test.mock.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 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 { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; + +export const mockCreateOrUpgradeSavedConfig = jest.fn() as jest.MockedFunction< + typeof createOrUpgradeSavedConfig +>; +jest.mock('./create_or_upgrade_saved_config', () => ({ + createOrUpgradeSavedConfig: mockCreateOrUpgradeSavedConfig, +})); diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 99ab87610db90..844e17e53ab89 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -10,7 +10,7 @@ import Chance from 'chance'; import { schema } from '@kbn/config-schema'; import { loggingSystemMock } from '../logging/logging_system.mock'; -import { createOrUpgradeSavedConfigMock } from './create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock'; +import { mockCreateOrUpgradeSavedConfig } from './ui_settings_client.test.mock'; import { SavedObjectsClient } from '../saved_objects'; import { savedObjectsClientMock } from '../saved_objects/service/saved_objects_client.mock'; @@ -47,12 +47,9 @@ describe('ui settings', () => { log: logger, }); - const createOrUpgradeSavedConfig = createOrUpgradeSavedConfigMock; - return { uiSettings, savedObjectsClient, - createOrUpgradeSavedConfig, }; } @@ -84,7 +81,7 @@ describe('ui settings', () => { }); it('automatically creates the savedConfig if it is missing', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); savedObjectsClient.update .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) .mockResolvedValueOnce({} as any); @@ -92,14 +89,14 @@ describe('ui settings', () => { await uiSettings.setMany({ foo: 'bar' }); expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledWith( expect.objectContaining({ handleWriteErrors: false }) ); }); it('only tried to auto create once and throws NotFound', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); savedObjectsClient.update.mockRejectedValue( SavedObjectsClient.errors.createGenericNotFoundError() ); @@ -112,8 +109,8 @@ describe('ui settings', () => { } expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledWith( expect.objectContaining({ handleWriteErrors: false }) ); }); @@ -374,7 +371,7 @@ describe('ui settings', () => { }); it('automatically creates the savedConfig if it is missing and returns empty object', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); savedObjectsClient.get = jest .fn() .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) @@ -384,15 +381,15 @@ describe('ui settings', () => { expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledWith( expect.objectContaining({ handleWriteErrors: true }) ); }); it('returns result of savedConfig creation in case of notFound error', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); - createOrUpgradeSavedConfig.mockResolvedValue({ foo: 'bar ' }); + const { uiSettings, savedObjectsClient } = setup(); + mockCreateOrUpgradeSavedConfig.mockResolvedValue({ foo: 'bar ' }); savedObjectsClient.get.mockRejectedValue( SavedObjectsClient.errors.createGenericNotFoundError() ); @@ -401,23 +398,23 @@ describe('ui settings', () => { }); it('returns an empty object on Forbidden responses', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); const error = SavedObjectsClient.errors.decorateForbiddenError(new Error()); savedObjectsClient.get.mockRejectedValue(error); expect(await uiSettings.getUserProvided()).toStrictEqual({}); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); it('returns an empty object on EsUnavailable responses', async () => { - const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); + const { uiSettings, savedObjectsClient } = setup(); const error = SavedObjectsClient.errors.decorateEsUnavailableError(new Error()); savedObjectsClient.get.mockRejectedValue(error); expect(await uiSettings.getUserProvided()).toStrictEqual({}); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); + expect(mockCreateOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); it('throws Unauthorized errors', async () => { diff --git a/src/core/test_helpers/http_test_setup.ts b/src/core/test_helpers/http_test_setup.ts index 67b340898aab4..2a7d6451319cb 100644 --- a/src/core/test_helpers/http_test_setup.ts +++ b/src/core/test_helpers/http_test_setup.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ +import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { HttpService } from '../public/http'; import { fatalErrorsServiceMock } from '../public/fatal_errors/fatal_errors_service.mock'; -import { injectedMetadataServiceMock } from '../public/injected_metadata/injected_metadata_service.mock'; import { executionContextServiceMock } from '../public/execution_context/execution_context_service.mock'; export type SetupTap = ( diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index 2b3bccc164e6d..00b1260b1f90f 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -76,7 +76,12 @@ export const DEV_ONLY_LICENSE_ALLOWED = ['MPL-2.0']; export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint +<<<<<<< HEAD '@elastic/ems-client@8.3.2': ['Elastic License 2.0'], '@elastic/eui@58.0.0': ['SSPL-1.0 OR Elastic License 2.0'], +======= + '@elastic/ems-client@8.3.3': ['Elastic License 2.0'], + '@elastic/eui@55.1.4': ['SSPL-1.0 OR Elastic License 2.0'], +>>>>>>> 7695aad7a16a3816fb343252581f9ca8c02d65a2 'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry }; diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 45b8aad7df8cf..702b63fe48fc0 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -38,5 +38,5 @@ export const storybookAliases = { presentation: 'src/plugins/presentation_util/storybook', security_solution: 'x-pack/plugins/security_solution/.storybook', shared_ux: 'packages/kbn-shared-ux-storybook/src/config', - ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook', + ui_actions_enhanced: 'src/plugins/ui_actions_enhanced/.storybook', }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 89a242fe26de1..93f9365987553 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -24,7 +24,7 @@ export interface Dimension { accessor: number; format: { id?: string; - params?: SerializedFieldFormat; + params?: SerializedFieldFormat; }; } @@ -129,7 +129,7 @@ export enum LegendDisplay { export interface BucketColumns extends DatatableColumn { format?: { id?: string; - params?: SerializedFieldFormat; + params?: SerializedFieldFormat; }; } diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index d4216545edd66..25314572529b4 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -18,12 +18,7 @@ import { XYChartSeriesIdentifier, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { - FieldFormat, - FieldFormatParams, - IFieldFormat, - SerializedFieldFormat, -} from '@kbn/field-formats-plugin/common'; +import { FieldFormat, IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import { Datatable } from '@kbn/expressions-plugin'; import { getFormatByAccessor, @@ -64,7 +59,7 @@ type GetSeriesNameFn = ( config: { splitColumnId?: string; accessorsCount: number; - splitHint: SerializedFieldFormat | undefined; + splitHint: SerializedFieldFormat | undefined; splitFormatter: FieldFormat; alreadyFormattedColumns: Record; columnToLabelMap: Record; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx index 0817b6df5f7ef..eb085248f4a3e 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -11,6 +11,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef } from 'react'; import { convertMapboxVectorTileToJson } from './mapbox_vector_tile'; +import { Mode } from '../../../../models/legacy_core_editor/mode/output'; // Ensure the modes we might switch to dynamically are available import 'brace/mode/text'; @@ -83,7 +84,10 @@ function EditorOutputUI() { useEffect(() => { const editor = editorInstanceRef.current!; if (data) { - const mode = modeForContentType(data[0].response.contentType); + const isMultipleRequest = data.length > 1; + const mode = isMultipleRequest + ? new Mode() + : modeForContentType(data[0].response.contentType); editor.update( data .map((result) => { diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts index 1ac47df30fca5..3247c8aed164e 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts @@ -9,8 +9,7 @@ import type { HttpSetup, IHttpFetchError } from '@kbn/core/public'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; import { extractWarningMessages } from '../../../lib/utils'; -// @ts-ignore -import * as es from '../../../lib/es/es'; +import { send } from '../../../lib/es/es'; import { BaseResponseType } from '../../../types'; const { collapseLiteralStrings } = XJson; @@ -72,7 +71,7 @@ export function sendRequest(args: RequestArgs): Promise { const startTime = Date.now(); try { - const { response, body } = await es.send({ + const { response, body } = await send({ http: args.http, method, path, @@ -106,7 +105,7 @@ export function sendRequest(args: RequestArgs): Promise { } if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; + value = `# ${req.method} ${req.url} ${response.status} ${response.statusText}\n${value}`; } results.push({ @@ -141,7 +140,7 @@ export function sendRequest(args: RequestArgs): Promise { } if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; + value = `# ${req.method} ${req.url} ${statusCode} ${statusText}\n${value}`; } const result = { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts b/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts index 2b87331d5f47d..7924b06e8b15f 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts @@ -8,12 +8,11 @@ import _ from 'lodash'; import ace from 'brace'; -// @ts-ignore -import * as OutputMode from './mode/output'; +import { Mode } from './mode/output'; import smartResize from './smart_resize'; export interface CustomAceEditor extends ace.Editor { - update: (text: string, mode?: unknown, cb?: () => void) => void; + update: (text: string, mode?: string | Mode, cb?: () => void) => void; append: (text: string, foldPrevious?: boolean, cb?: () => void) => void; } @@ -24,19 +23,23 @@ export interface CustomAceEditor extends ace.Editor { export function createReadOnlyAceEditor(element: HTMLElement): CustomAceEditor { const output: CustomAceEditor = ace.acequire('ace/ace').edit(element); - const outputMode = new OutputMode.Mode(); + const outputMode = new Mode(); output.$blockScrolling = Infinity; output.resize = smartResize(output); - output.update = (val: string, mode?: unknown, cb?: () => void) => { + output.update = (val, mode, cb) => { if (typeof mode === 'function') { cb = mode as () => void; mode = void 0; } const session = output.getSession(); + const currentMode = val ? mode || outputMode : 'ace/mode/text'; - session.setMode(val ? mode || outputMode : 'ace/mode/text'); + // @ts-ignore + // ignore ts error here due to type definition mistake in brace for setMode(mode: string): void; + // this method accepts string or SyntaxMode which is an object. See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L467 + session.setMode(currentMode); session.setValue(val); if (typeof cb === 'function') { setTimeout(cb); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts similarity index 75% rename from src/plugins/console/public/application/models/legacy_core_editor/mode/output.js rename to src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts index b769505e81335..234d57b830a7b 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts @@ -10,7 +10,6 @@ import ace from 'brace'; import { OutputJsonHighlightRules } from './output_highlight_rules'; -const oop = ace.acequire('ace/lib/oop'); const JSONMode = ace.acequire('ace/mode/json').Mode; const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent; const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour; @@ -18,15 +17,17 @@ const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode; ace.acequire('ace/worker/worker_client'); const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; -export function Mode() { - this.$tokenizer = new AceTokenizer(new OutputJsonHighlightRules().getRules()); - this.$outdent = new MatchingBraceOutdent(); - this.$behaviour = new CstyleBehaviour(); - this.foldingRules = new CStyleFoldMode(); +export class Mode extends JSONMode { + constructor() { + super(); + this.$tokenizer = new AceTokenizer(new OutputJsonHighlightRules().getRules()); + this.$outdent = new MatchingBraceOutdent(); + this.$behaviour = new CstyleBehaviour(); + this.foldingRules = new CStyleFoldMode(); + } } -oop.inherits(Mode, JSONMode); -(function () { +(function (this: Mode) { this.createWorker = function () { return null; }; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js deleted file mode 100644 index ebcce29da9e1e..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js +++ /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 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 ace from 'brace'; -import 'brace/mode/json'; -import { addXJsonToRules } from '@kbn/ace'; - -const oop = ace.acequire('ace/lib/oop'); -const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHighlightRules; - -export function OutputJsonHighlightRules() { - this.$rules = {}; - - addXJsonToRules(this, 'start'); - - this.$rules.start.unshift( - { - token: 'warning', - regex: '#!.*$', - }, - { - token: 'comment', - regex: '#.*$', - } - ); - - if (this.constructor === OutputJsonHighlightRules) { - this.normalizeRules(); - } -} - -oop.inherits(OutputJsonHighlightRules, JsonHighlightRules); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts new file mode 100644 index 0000000000000..cdbbd4bc7b178 --- /dev/null +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { mapStatusCodeToBadge } from './output_highlight_rules'; + +describe('mapStatusCodeToBadge', () => { + const testCases = [ + { + description: 'treats 100 as as default', + value: '# PUT test-index 100 Continue', + badge: 'badge.badge--default', + }, + { + description: 'treats 200 as success', + value: '# PUT test-index 200 OK', + badge: 'badge.badge--success', + }, + { + description: 'treats 301 as primary', + value: '# PUT test-index 301 Moved Permanently', + badge: 'badge.badge--primary', + }, + { + description: 'treats 400 as warning', + value: '# PUT test-index 404 Not Found', + badge: 'badge.badge--warning', + }, + { + description: 'treats 502 as danger', + value: '# PUT test-index 502 Bad Gateway', + badge: 'badge.badge--danger', + }, + { + description: 'treats unexpected numbers as danger', + value: '# PUT test-index 666 Demonic Invasion', + badge: 'badge.badge--danger', + }, + { + description: 'treats no numbers as undefined', + value: '# PUT test-index', + badge: undefined, + }, + ]; + + testCases.forEach(({ description, value, badge }) => { + test(description, () => { + expect(mapStatusCodeToBadge(value)).toBe(badge); + }); + }); +}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts new file mode 100644 index 0000000000000..925bcde746b85 --- /dev/null +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 ace from 'brace'; +import 'brace/mode/json'; +import { addXJsonToRules } from '@kbn/ace'; + +const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHighlightRules; + +export const mapStatusCodeToBadge = (value: string) => { + const regExpMatchArray = value.match(/\d+/); + if (regExpMatchArray) { + const status = parseInt(regExpMatchArray[0], 10); + if (status <= 199) { + return 'badge.badge--default'; + } + if (status <= 299) { + return 'badge.badge--success'; + } + if (status <= 399) { + return 'badge.badge--primary'; + } + if (status <= 499) { + return 'badge.badge--warning'; + } + return 'badge.badge--danger'; + } +}; + +export class OutputJsonHighlightRules extends JsonHighlightRules { + constructor() { + super(); + this.$rules = {}; + addXJsonToRules(this, 'start'); + this.$rules.start.unshift( + { + token: 'warning', + regex: '#!.*$', + }, + { + token: 'comment', + regex: /#(.*?)(?=\d+\s(?:[\sA-Za-z]+)|$)/, + }, + { + token: mapStatusCodeToBadge, + regex: /(\d+\s[\sA-Za-z]+$)/, + } + ); + + if (this instanceof OutputJsonHighlightRules) { + this.normalizeRules(); + } + } +} diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index 61dc31138c768..2490bb29f0fb7 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -33,6 +33,46 @@ .conApp__output { display: flex; flex: 1 1 1px; + + .ace_badge { + font-family: $euiFontFamily; + font-size: $euiFontSizeXS; + font-weight: $euiFontWeightMedium; + line-height: $euiLineHeight; + padding: 0 $euiSizeS; + display: inline-block; + text-decoration: none; + border-radius: $euiBorderRadius / 2; + white-space: nowrap; + vertical-align: middle; + cursor: default; + max-width: 100%; + + &--success { + background-color: $euiColorVis0_behindText; + color: chooseLightOrDarkText($euiColorVis0_behindText); + } + + &--warning { + background-color: $euiColorVis5_behindText; + color: chooseLightOrDarkText($euiColorVis5_behindText); + } + + &--primary { + background-color: $euiColorVis1_behindText; + color: chooseLightOrDarkText($euiColorVis1_behindText); + } + + &--default { + background-color: $euiColorLightShade; + color: chooseLightOrDarkText($euiColorLightShade); + } + + &--danger { + background-color: $euiColorVis9_behindText; + color: chooseLightOrDarkText($euiColorVis9_behindText); + } + } } .conApp__editorContent, diff --git a/src/plugins/console/server/lib/proxy_request.test.ts b/src/plugins/console/server/lib/proxy_request.test.ts index 2bb5e481fbb26..8eced59cf2cea 100644 --- a/src/plugins/console/server/lib/proxy_request.test.ts +++ b/src/plugins/console/server/lib/proxy_request.test.ts @@ -149,5 +149,21 @@ describe(`Console's send request`, () => { const [httpRequestOptions] = stub.firstCall.args; expect((httpRequestOptions as any).path).toEqual('/%3Cmy-index-%7Bnow%2Fd%7D%3E'); }); + + it('should not encode path if it does not require encoding', async () => { + const result = await proxyRequest({ + agent: null as any, + headers: {}, + method: 'get', + payload: null as any, + timeout: 30000, + uri: new URL(`http://noone.nowhere.none/my-index/_doc/this%2Fis%2Fa%2Fdoc`), + originalPath: 'my-index/_doc/this%2Fis%2Fa%2Fdoc', + }); + + expect(result).toEqual('done'); + const [httpRequestOptions] = stub.firstCall.args; + expect((httpRequestOptions as any).path).toEqual('/my-index/_doc/this%2Fis%2Fa%2Fdoc'); + }); }); }); diff --git a/src/plugins/console/server/lib/proxy_request.ts b/src/plugins/console/server/lib/proxy_request.ts index c4fbfd315da4e..62437acbd0ddd 100644 --- a/src/plugins/console/server/lib/proxy_request.ts +++ b/src/plugins/console/server/lib/proxy_request.ts @@ -22,6 +22,7 @@ interface Args { timeout: number; headers: http.OutgoingHttpHeaders; rejectUnauthorized?: boolean; + originalPath?: string; } /** @@ -39,11 +40,6 @@ const sanitizeHostname = (hostName: string): string => const encodePathname = (pathname: string) => { const decodedPath = new URLSearchParams(`path=${pathname}`).get('path') ?? ''; - // Skip if it is valid - if (pathname === decodedPath) { - return pathname; - } - return `/${encodeURIComponent(trimStart(decodedPath, '/'))}`; }; @@ -58,11 +54,17 @@ export const proxyRequest = ({ timeout, payload, rejectUnauthorized, + originalPath, }: Args) => { - const { hostname, port, protocol, pathname, search } = uri; + const { hostname, port, protocol, search, pathname: percentEncodedPath } = uri; const client = uri.protocol === 'https:' ? https : http; - const encodedPath = encodePathname(pathname); + let pathname = uri.pathname; let resolved = false; + const requiresEncoding = trimStart(originalPath, '/') !== trimStart(percentEncodedPath, '/'); + + if (requiresEncoding) { + pathname = encodePathname(pathname); + } let resolve: (res: http.IncomingMessage) => void; let reject: (res: unknown) => void; @@ -84,7 +86,7 @@ export const proxyRequest = ({ host: sanitizeHostname(hostname), port: port === '' ? undefined : parseInt(port, 10), protocol, - path: `${encodedPath}${search || ''}`, + path: `${pathname}${search || ''}`, headers: { ...finalUserHeaders, 'content-type': 'application/json', diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts index 8cd0400d82cb0..9b9cb0f3b66ef 100644 --- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts +++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts @@ -175,6 +175,7 @@ export const createHandler = payload: body, rejectUnauthorized, agent, + originalPath: path, }); break; diff --git a/src/plugins/controls/common/control_group/types.ts b/src/plugins/controls/common/control_group/types.ts index be24d8790dae8..65c29c3bac7c5 100644 --- a/src/plugins/controls/common/control_group/types.ts +++ b/src/plugins/controls/common/control_group/types.ts @@ -7,6 +7,7 @@ */ import { EmbeddableInput, PanelState } from '@kbn/embeddable-plugin/common/types'; +import { SerializableRecord } from '@kbn/utility-types'; import { ControlInput, ControlStyle, ControlWidth } from '../types'; export const CONTROL_GROUP_TYPE = 'control_group'; @@ -32,12 +33,25 @@ export interface ControlGroupInput extends EmbeddableInput, ControlInput { panels: ControlsPanels; } -// only parts of the Control Group Input should be persisted +/** + * Only parts of the Control Group Input should be persisted + */ export type PersistableControlGroupInput = Pick< ControlGroupInput, 'panels' | 'chainingSystem' | 'controlStyle' | 'ignoreParentSettings' >; +/** + * Some use cases need the Persistable Control Group Input to conform to the SerializableRecord format which requires string index signatures in any objects + */ +export type SerializableControlGroupInput = Omit< + PersistableControlGroupInput, + 'panels' | 'ignoreParentSettings' +> & { + panels: ControlsPanels & SerializableRecord; + ignoreParentSettings: PersistableControlGroupInput['ignoreParentSettings'] & SerializableRecord; +}; + // panels are json stringified for storage in a saved object. export type RawControlGroupAttributes = Omit< PersistableControlGroupInput, @@ -46,6 +60,7 @@ export type RawControlGroupAttributes = Omit< ignoreParentSettingsJSON: string; panelsJSON: string; }; + export interface ControlGroupTelemetry { total: number; chaining_system: { diff --git a/src/plugins/controls/common/index.ts b/src/plugins/controls/common/index.ts index 2956570f79189..3066629810df4 100644 --- a/src/plugins/controls/common/index.ts +++ b/src/plugins/controls/common/index.ts @@ -11,12 +11,13 @@ export type { ControlWidth } from './types'; // Control Group exports export { CONTROL_GROUP_TYPE, - type ControlPanelState, type ControlsPanels, type ControlGroupInput, + type ControlPanelState, type ControlGroupTelemetry, type RawControlGroupAttributes, type PersistableControlGroupInput, + type SerializableControlGroupInput, } from './control_group/types'; export { controlGroupInputToRawControlGroupAttributes, diff --git a/src/plugins/controls/common/mocks.tsx b/src/plugins/controls/common/mocks.tsx new file mode 100644 index 0000000000000..8574c78ef40c7 --- /dev/null +++ b/src/plugins/controls/common/mocks.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 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 { getDefaultControlGroupInput } from '.'; +import { ControlGroupInput } from './control_group/types'; + +export const mockControlGroupInput = (partial?: Partial): ControlGroupInput => ({ + id: 'mocked_control_group', + ...getDefaultControlGroupInput(), + ...{ + panels: { + control1: { + order: 0, + width: 'medium', + grow: true, + type: 'mockedOptionsList', + explicitInput: { + id: 'control1', + }, + }, + control2: { + order: 1, + width: 'large', + grow: true, + type: 'mockedRangeSlider', + explicitInput: { + id: 'control2', + }, + }, + control3: { + order: 2, + width: 'small', + grow: true, + type: 'mockedOptionsList', + explicitInput: { + id: 'control3', + }, + }, + }, + }, + ...(partial ?? {}), +}); diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx index 7ce3a139f773a..ca14d123dc991 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx @@ -118,14 +118,16 @@ export class DashboardViewport extends React.Component ) : null} -
0 - ? 'dshDashboardViewport-controls' - : '' - } - ref={this.controlsRoot} - /> + {container.getInput().viewMode !== ViewMode.PRINT && ( +
0 + ? 'dshDashboardViewport-controls' + : '' + } + ref={this.controlsRoot} + /> + )} ) : null}
= {}; const unsavedDashboardState = dashboardSessionStorage.getState(savedDashboard.id); if (unsavedDashboardState) { @@ -125,6 +126,7 @@ export function ShowShareModal({ filters: unsavedDashboardState.filters, options: unsavedDashboardState.options, savedQuery: unsavedDashboardState.savedQuery, + controlGroupInput: unsavedDashboardState.controlGroupInput as SerializableControlGroupInput, panels: unsavedDashboardState.panels ? convertPanelMapToSavedPanels(unsavedDashboardState.panels, kibanaVersion) : undefined, diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts index bc82477beb192..402378162fa1f 100644 --- a/src/plugins/dashboard/public/locator.test.ts +++ b/src/plugins/dashboard/public/locator.test.ts @@ -9,7 +9,9 @@ import { DashboardAppLocatorDefinition } from './locator'; import { hashedItemStore } from '@kbn/kibana-utils-plugin/public'; import { mockStorage } from '@kbn/kibana-utils-plugin/public/storage/hashed_item_store/mock'; +import { mockControlGroupInput } from '@kbn/controls-plugin/common/mocks'; import { FilterStateStore } from '@kbn/es-query'; +import { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; describe('dashboard locator', () => { beforeEach(() => { @@ -204,6 +206,25 @@ describe('dashboard locator', () => { }); }); + test('Control Group Input', async () => { + const definition = new DashboardAppLocatorDefinition({ + useHashedUrl: false, + getDashboardFilterFields: async (dashboardId: string) => [], + }); + const controlGroupInput = mockControlGroupInput() as unknown as SerializableControlGroupInput; + const location = await definition.getLocation({ + controlGroupInput, + }); + + expect(location).toMatchObject({ + app: 'dashboards', + path: `#/create?_g=()`, + state: { + controlGroupInput, + }, + }); + }); + test('if no useHash setting is given, uses the one was start services', async () => { const definition = new DashboardAppLocatorDefinition({ useHashedUrl: true, diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index 7649343e5bf6e..ebf7b13a9563a 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -16,8 +16,10 @@ import type { RefreshInterval, } from '@kbn/data-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; +import { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; + import type { SavedDashboardPanel } from '../common/types'; import type { RawDashboardState } from './types'; import { DashboardConstants } from './dashboard_constants'; @@ -105,6 +107,11 @@ export type DashboardAppLocatorParams = { savedQuery?: string; options?: RawDashboardState['options']; + + /** + * Control group input + */ + controlGroupInput?: SerializableControlGroupInput; }; export type DashboardAppLocator = LocatorPublic; diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index b7ecbc05b2a33..98ffaf9d125b1 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -100,27 +100,16 @@ export { CSV_MIME_TYPE, } from './exports'; export type { - IFieldType, - IIndexPatternFieldList, - FieldFormatMap, - RuntimeType, - RuntimeField, DataViewAttributes, - IndexPatternAttributes, FieldAttrs, FieldAttrSet, - OnNotification, - OnError, UiSettingsCommon, - SavedObjectsClientCommonFindArgs, - SavedObjectsClientCommon, GetFieldsOptions, IDataViewsApiClient, SavedObject, AggregationRestrictions, TypeMeta, FieldSpecConflictDescriptions, - FieldSpecExportFmt, FieldSpec, DataViewFieldMap, DataViewSpec, @@ -129,11 +118,7 @@ export type { IndexPatternLoadStartDependencies, IndexPatternLoadExpressionFunctionDefinition, } from '@kbn/data-views-plugin/common'; -export type { - IndexPatternsContract, - DataViewsContract, - DataViewListItem, -} from '@kbn/data-views-plugin/common'; +export type { DataViewsContract, DataViewListItem } from '@kbn/data-views-plugin/common'; export { RUNTIME_FIELD_TYPES, DEFAULT_ASSETS_TO_IGNORE, @@ -143,7 +128,6 @@ export { fieldList, DataViewField, DataViewType, - IndexPatternsService, DataViewsService, DataView, DuplicateDataViewError, diff --git a/src/plugins/data/common/search/aggs/agg_config.test.ts b/src/plugins/data/common/search/aggs/agg_config.test.ts index 34c1acfc65ca0..4e7d408e56ae9 100644 --- a/src/plugins/data/common/search/aggs/agg_config.test.ts +++ b/src/plugins/data/common/search/aggs/agg_config.test.ts @@ -15,7 +15,11 @@ import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockAggTypesRegistry } from './test_helpers'; import { MetricAggType } from './metrics/metric_agg_type'; -import type { DataView, DataViewField, IIndexPatternFieldList } from '../..'; +import type { + DataView, + DataViewField, + IIndexPatternFieldList, +} from '@kbn/data-views-plugin/common'; describe('AggConfig', () => { let indexPattern: DataView; @@ -65,7 +69,7 @@ describe('AggConfig', () => { describe('#toDsl', () => { it('calls #write()', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { enabled: true, type: 'date_histogram', @@ -80,7 +84,7 @@ describe('AggConfig', () => { }); it('uses the type name as the agg name', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { enabled: true, type: 'date_histogram', @@ -95,7 +99,7 @@ describe('AggConfig', () => { }); it('uses the params from #write() output as the agg params', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { enabled: true, type: 'date_histogram', @@ -125,7 +129,7 @@ describe('AggConfig', () => { params: {}, }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const histoConfig = ac.byName('date_histogram')[0]; const avgConfig = ac.byName('avg')[0]; @@ -164,7 +168,7 @@ describe('AggConfig', () => { params: {}, }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const histoConfig = ac.byName('date_histogram')[0]; const avgConfig = ac.byName('avg')[0]; @@ -280,8 +284,8 @@ describe('AggConfig', () => { testsIdentical.forEach((configState, index) => { it(`identical aggregations (${index})`, () => { - const ac1 = new AggConfigs(indexPattern, configState, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, configState, { typesRegistry }); + const ac1 = new AggConfigs(indexPattern, configState, { typesRegistry }, jest.fn()); + const ac2 = new AggConfigs(indexPattern, configState, { typesRegistry }, jest.fn()); expect(ac1.jsonDataEquals(ac2.aggs)).toBe(true); }); }); @@ -321,8 +325,8 @@ describe('AggConfig', () => { testsIdenticalDifferentOrder.forEach((test, index) => { it(`identical aggregations (${index}) - init json is in different order`, () => { - const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }); + const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }, jest.fn()); + const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }, jest.fn()); expect(ac1.jsonDataEquals(ac2.aggs)).toBe(true); }); }); @@ -386,8 +390,8 @@ describe('AggConfig', () => { testsDifferent.forEach((test, index) => { it(`different aggregations (${index})`, () => { - const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }); + const ac1 = new AggConfigs(indexPattern, test.config1, { typesRegistry }, jest.fn()); + const ac2 = new AggConfigs(indexPattern, test.config2, { typesRegistry }, jest.fn()); expect(ac1.jsonDataEquals(ac2.aggs)).toBe(false); }); }); @@ -395,7 +399,7 @@ describe('AggConfig', () => { describe('#serialize', () => { it('includes the aggs id, params, type and schema', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { enabled: true, type: 'date_histogram', @@ -426,8 +430,8 @@ describe('AggConfig', () => { params: {}, }, ]; - const ac1 = new AggConfigs(indexPattern, configStates, { typesRegistry }); - const ac2 = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac1 = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); + const ac2 = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); // this relies on the assumption that js-engines consistently loop over properties in insertion order. // most likely the case, but strictly speaking not guaranteed by the JS and JSON specifications. @@ -455,7 +459,7 @@ describe('AggConfig', () => { params: { field: 'machine.os.keyword' }, }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(ac.aggs.map((agg) => agg.toSerializedFieldFormat())).toMatchInlineSnapshot(` Array [ @@ -517,7 +521,7 @@ describe('AggConfig', () => { }, }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(ac.aggs.map((agg) => agg.toSerializedFieldFormat())).toMatchInlineSnapshot(` Array [ @@ -540,7 +544,7 @@ describe('AggConfig', () => { describe('#toExpressionAst', () => { it('works with primitive param types', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { enabled: true, type: 'terms', @@ -597,7 +601,7 @@ describe('AggConfig', () => { }); it('creates a subexpression for params of type "agg"', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { type: 'terms', params: { @@ -688,7 +692,7 @@ describe('AggConfig', () => { return Array.isArray(val) ? val.map(toExpression) : toExpression(val); }; - ac = new AggConfigs(indexPattern, [], { typesRegistry }); + ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); }); it('creates a subexpression for param types other than "agg" which have specified toExpressionAst', () => { @@ -775,7 +779,7 @@ describe('AggConfig', () => { }); it('stringifies any other params which are an object', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { type: 'terms', params: { @@ -790,7 +794,7 @@ describe('AggConfig', () => { }); it('stringifies arrays only if they are objects', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { type: 'range', params: { @@ -808,7 +812,7 @@ describe('AggConfig', () => { }); it('does not stringify arrays which are not objects', () => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); const configStates = { type: 'percentiles', params: { @@ -826,7 +830,7 @@ describe('AggConfig', () => { let aggConfig: AggConfig; beforeEach(() => { - const ac = new AggConfigs(indexPattern, [], { typesRegistry }); + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); aggConfig = ac.createAggConfig({ type: 'count' } as CreateAggConfigParams); }); diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index f78452d99eded..e47fc12eca446 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -14,6 +14,7 @@ import { Assign, Ensure } from '@kbn/utility-types'; import { ExpressionAstExpression, ExpressionAstArgument } from '@kbn/expressions-plugin/common'; import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; +import { FieldFormatParams } from '@kbn/field-formats-plugin/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ISearchOptions, ISearchSource } from '../../../public'; @@ -185,7 +186,9 @@ export class AggConfig { return; } const resolvedBounds = this.aggConfigs.getResolvedTimeRange()!; - return moment.duration(moment(resolvedBounds.max).diff(resolvedBounds.min)); + return moment.duration( + moment.tz(resolvedBounds.max, this.aggConfigs.timeZone).diff(resolvedBounds.min) + ); } return parsedTimeShift; } @@ -323,9 +326,7 @@ export class AggConfig { * * @public */ - toSerializedFieldFormat(): - | {} - | Ensure, SerializableRecord> { + toSerializedFieldFormat(): SerializedFieldFormat { return this.type ? this.type.getSerializedFormat(this) : {}; } diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts index 8ea53ddd3270c..3f629dc8d1be9 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts @@ -7,6 +7,7 @@ */ import { keyBy } from 'lodash'; +import { ExpressionAstExpression, buildExpression } from '@kbn/expressions-plugin/common'; import { AggConfig } from './agg_config'; import { AggConfigs } from './agg_configs'; import { AggTypesRegistryStart } from './agg_types_registry'; @@ -33,7 +34,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(ac.aggs).toHaveLength(1); }); @@ -58,7 +59,7 @@ describe('AggConfigs', () => { ]; const spy = jest.spyOn(AggConfig, 'ensureIds'); - new AggConfigs(indexPattern, configStates, { typesRegistry }); + new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(spy).toHaveBeenCalledTimes(1); expect(spy.mock.calls[0]).toEqual([configStates]); spy.mockRestore(); @@ -80,7 +81,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(ac.aggs).toHaveLength(2); ac.createAggConfig( @@ -103,7 +104,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(ac.aggs).toHaveLength(1); ac.createAggConfig({ @@ -124,7 +125,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(ac.aggs).toHaveLength(1); ac.createAggConfig( @@ -148,7 +149,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(() => ac.createAggConfig({ enabled: true, @@ -173,7 +174,7 @@ describe('AggConfigs', () => { { type: 'percentiles', enabled: true, params: {}, schema: 'metric' }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const sorted = ac.getRequestAggs(); const aggs = keyBy(ac.aggs, (agg) => agg.type.name); @@ -196,7 +197,7 @@ describe('AggConfigs', () => { { type: 'count', enabled: true, params: {}, schema: 'metric' }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const sorted = ac.getResponseAggs(); const aggs = keyBy(ac.aggs, (agg) => agg.type.name); @@ -213,7 +214,7 @@ describe('AggConfigs', () => { { type: 'percentiles', enabled: true, params: { percents: [1, 2, 3] }, schema: 'metric' }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const sorted = ac.getResponseAggs(); const aggs = keyBy(ac.aggs, (agg) => agg.type.name); @@ -234,7 +235,7 @@ describe('AggConfigs', () => { { id: '101', type: 'count', enabled: true, params: {}, schema: 'metric' }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(ac.getResponseAggById('1')?.type.name).toEqual('terms'); expect(ac.getResponseAggById('10')?.type.name).toEqual('date_histogram'); expect(ac.getResponseAggById('101')?.type.name).toEqual('count'); @@ -253,7 +254,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); expect(ac.getResponseAggById('1')?.type.name).toEqual('terms'); expect(ac.getResponseAggById('10')?.type.name).toEqual('date_histogram'); expect(ac.getResponseAggById('101.1')?.type.name).toEqual('percentiles'); @@ -265,7 +266,7 @@ describe('AggConfigs', () => { describe('#toDsl', () => { it('uses the sorted aggs', () => { const configStates = [{ enabled: true, type: 'avg', params: { field: 'bytes' } }]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const spy = jest.spyOn(AggConfigs.prototype, 'getRequestAggs'); ac.toDsl(); expect(spy).toHaveBeenCalledTimes(1); @@ -279,7 +280,7 @@ describe('AggConfigs', () => { { enabled: true, type: 'count', params: {} }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const aggInfos = ac.aggs.map((aggConfig) => { const football = {}; @@ -322,7 +323,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const dsl = ac.toDsl(); const histo = ac.byName('date_histogram')[0]; const count = ac.byName('count')[0]; @@ -347,7 +348,7 @@ describe('AggConfigs', () => { { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); const dsl = ac.toDsl(); const histo = ac.byName('date_histogram')[0]; const metrics = ac.bySchemaName('metrics'); @@ -379,7 +380,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); ac.timeFields = ['@timestamp']; ac.timeRange = { from: '2021-05-05T00:00:00.000Z', @@ -442,7 +443,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); ac.timeFields = ['timestamp']; ac.timeRange = { from: '2021-05-05T00:00:00.000Z', @@ -469,7 +470,12 @@ describe('AggConfigs', () => { { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, hierarchical: true }); + const ac = new AggConfigs( + indexPattern, + configStates, + { typesRegistry, hierarchical: true }, + jest.fn() + ); const topLevelDsl = ac.toDsl(); const buckets = ac.bySchemaName('buckets'); const metrics = ac.bySchemaName('metrics'); @@ -539,7 +545,12 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, hierarchical: true }); + const ac = new AggConfigs( + indexPattern, + configStates, + { typesRegistry, hierarchical: true }, + jest.fn() + ); const topLevelDsl = ac.toDsl()['2']; expect(Object.keys(topLevelDsl.aggs)).toContain('1'); @@ -586,7 +597,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); ac.timeFields = ['@timestamp']; ac.timeRange = { from: '2021-05-05T00:00:00.000Z', @@ -728,7 +739,7 @@ describe('AggConfigs', () => { }, ]; - const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); ac.timeFields = ['@timestamp']; ac.timeRange = { from: '2021-05-05T00:00:00.000Z', @@ -787,4 +798,62 @@ describe('AggConfigs', () => { }); }); }); + + describe('#toExpressionAst', () => { + function toString(ast: ExpressionAstExpression) { + return buildExpression(ast).toString(); + } + + it('should generate the `index` argument', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); + + expect(toString(ac.toExpressionAst())).toMatchInlineSnapshot( + `"esaggs index={indexPatternLoad id=\\"logstash-*\\"} metricsAtAllLevels=false partialRows=false"` + ); + }); + + it('should generate the `metricsAtAllLevels` if hierarchical', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); + ac.hierarchical = true; + + expect(toString(ac.toExpressionAst())).toMatchInlineSnapshot( + `"esaggs index={indexPatternLoad id=\\"logstash-*\\"} metricsAtAllLevels=true partialRows=false"` + ); + }); + + it('should generate the `partialRows` argument', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry }, jest.fn()); + ac.partialRows = true; + + expect(toString(ac.toExpressionAst())).toMatchInlineSnapshot( + `"esaggs index={indexPatternLoad id=\\"logstash-*\\"} metricsAtAllLevels=false partialRows=true"` + ); + }); + + it('should generate the `aggs` argument', () => { + const configStates = [ + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'min', schema: 'metric', params: { field: 'bytes' } }, + { enabled: true, type: 'max', schema: 'metric', params: { field: 'bytes' } }, + ]; + + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }, jest.fn()); + + expect(toString(ac.toExpressionAst())).toMatchInlineSnapshot(` + "esaggs index={indexPatternLoad id=\\"logstash-*\\"} metricsAtAllLevels=false partialRows=false + aggs={aggDateHistogram field=\\"@timestamp\\" useNormalizedEsInterval=true extendToTimeRange=false scaleMetricValues=false interval=\\"10s\\" drop_partials=false min_doc_count=1 extended_bounds={extendedBounds} id=\\"1\\" enabled=true schema=\\"segment\\"} + aggs={aggAvg field=\\"bytes\\" id=\\"2\\" enabled=true schema=\\"metric\\"} + aggs={aggSum field=\\"bytes\\" emptyAsNull=false id=\\"3\\" enabled=true schema=\\"metric\\"} + aggs={aggMin field=\\"bytes\\" id=\\"4\\" enabled=true schema=\\"metric\\"} + aggs={aggMax field=\\"bytes\\" id=\\"5\\" enabled=true schema=\\"metric\\"}" + `); + }); + }); }); diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index fcb51b63df668..d22175ac7e4b6 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -6,13 +6,15 @@ * Side Public License, v 1. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import _, { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { isRangeFilter } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IndexPatternLoadExpressionFunctionDefinition } from '@kbn/data-views-plugin/common'; +import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common'; import { IEsSearchResponse, @@ -21,10 +23,12 @@ import { RangeFilter, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../public'; +import type { EsaggsExpressionFunctionDefinition } from '../expressions'; import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config'; import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; +import { AggTypesDependencies, GetConfigFn, getUserTimeZone } from '../..'; import { TimeRange, getTime, calculateBounds } from '../..'; import { IBucketAggConfig } from './buckets'; import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits'; @@ -55,6 +59,8 @@ function parseParentAggs(dslLvlCursor: any, dsl: any) { export interface AggConfigsOptions { typesRegistry: AggTypesRegistryStart; hierarchical?: boolean; + aggExecutionContext?: AggTypesDependencies['aggExecutionContext']; + partialRows?: boolean; } export type CreateAggConfigParams = Assign; @@ -78,29 +84,29 @@ export type GenericBucket = estypes.AggregationsBuckets & { export type IAggConfigs = AggConfigs; export class AggConfigs { - public indexPattern: DataView; public timeRange?: TimeRange; public timeFields?: string[]; public forceNow?: Date; - public hierarchical?: boolean = false; - - private readonly typesRegistry: AggTypesRegistryStart; - - aggs: IAggConfig[]; + public aggs: IAggConfig[] = []; + public partialRows?: boolean; + public hierarchical?: boolean; + public readonly timeZone: string; constructor( - indexPattern: DataView, + public indexPattern: DataView, configStates: CreateAggConfigParams[] = [], - opts: AggConfigsOptions + private opts: AggConfigsOptions, + private getConfig: GetConfigFn ) { - this.typesRegistry = opts.typesRegistry; - - configStates = AggConfig.ensureIds(configStates); + this.hierarchical = opts.hierarchical ?? false; + this.partialRows = opts.partialRows ?? false; - this.aggs = []; - this.indexPattern = indexPattern; - this.hierarchical = opts.hierarchical; + this.timeZone = getUserTimeZone( + this.getConfig, + opts?.aggExecutionContext?.shouldDetectTimeZone + ); + configStates = AggConfig.ensureIds(configStates); configStates.forEach((params: any) => this.createAggConfig(params)); } @@ -149,11 +155,16 @@ export class AggConfigs { return agg.enabled; }; - const aggConfigs = new AggConfigs(this.indexPattern, this.aggs.filter(filterAggs), { - typesRegistry: this.typesRegistry, - }); - - return aggConfigs; + return new AggConfigs( + this.indexPattern, + this.aggs.filter(filterAggs), + { + ...this.opts, + hierarchical: this.hierarchical, + partialRows: this.partialRows, + }, + this.getConfig + ); } createAggConfig = ( @@ -162,7 +173,7 @@ export class AggConfigs { ) => { const { type } = params; const getType = (t: string) => { - const typeFromRegistry = this.typesRegistry.get(t); + const typeFromRegistry = this.opts.typesRegistry.get(t); if (!typeFromRegistry) { throw new Error( @@ -256,7 +267,7 @@ export class AggConfigs { } if (hasMultipleTimeShifts) { - dslLvlCursor = insertTimeShiftSplit(this, config, timeShifts, dslLvlCursor); + dslLvlCursor = insertTimeShiftSplit(this, config, timeShifts, dslLvlCursor, this.timeZone); } if (config.type.hasNoDsl) { @@ -408,8 +419,14 @@ export class AggConfigs { range: { [field]: { format: 'strict_date_optional_time', - gte: moment(filter?.query.range[field].gte).subtract(shift).toISOString(), - lte: moment(filter?.query.range[field].lte).subtract(shift).toISOString(), + gte: moment + .tz(filter?.query.range[field].gte, this.timeZone) + .subtract(shift) + .toISOString(), + lte: moment + .tz(filter?.query.range[field].lte, this.timeZone) + .subtract(shift) + .toISOString(), }, }, })), @@ -493,4 +510,26 @@ export class AggConfigs { this.getRequestAggs().map((agg: AggConfig) => agg.onSearchRequestStart(searchSource, options)) ); } + + /** + * Generates an expression abstract syntax tree using the `esaggs` expression function. + * @returns The expression AST. + */ + toExpressionAst() { + return buildExpression([ + buildExpressionFunction('esaggs', { + index: buildExpression([ + buildExpressionFunction( + 'indexPatternLoad', + { + id: this.indexPattern.id!, + } + ), + ]), + metricsAtAllLevels: this.hierarchical, + partialRows: this.partialRows, + aggs: this.aggs.map((agg) => buildExpression(agg.toExpressionAst())), + }), + ]).toAst(); + } } diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 633bd00dd9a35..22055587eb7e8 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -14,6 +14,7 @@ import type { RequestAdapter } from '@kbn/inspector-plugin/common'; import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { FieldFormatParams } from '@kbn/field-formats-plugin/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ISearchSource } from '../../../public'; import { initParams } from './agg_params'; @@ -208,7 +209,7 @@ export class AggType< * @param {agg} agg - the agg to pick a format for * @return {SerializedFieldFormat} */ - getSerializedFormat: (agg: TAggConfig) => SerializedFieldFormat; + getSerializedFormat: (agg: TAggConfig) => SerializedFieldFormat; getValue: (agg: TAggConfig, bucket: any) => any; diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index 0cbc3664a8659..396ca49edaf4f 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -18,7 +18,9 @@ export interface AggTypesDependencies { calculateBounds: CalculateBoundsFn; getConfig: (key: string) => T; getFieldFormatsStart: () => Pick; - isDefaultTimezone: () => boolean; + aggExecutionContext?: { + shouldDetectTimeZone?: boolean; + }; } /** @internal */ diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index 46d0921426de0..5f74605b0d3a5 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -23,7 +23,6 @@ describe('Aggs service', () => { calculateBounds: jest.fn(), getFieldFormatsStart: jest.fn(), getConfig: jest.fn(), - isDefaultTimezone: () => true, }; beforeEach(() => { @@ -34,7 +33,6 @@ describe('Aggs service', () => { startDeps = { getConfig: jest.fn(), getIndexPattern: jest.fn(), - isDefaultTimezone: jest.fn(), }; }); diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts index 21010a9294b8c..694be7019fa55 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -8,7 +8,7 @@ import { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { CreateAggConfigParams, UI_SETTINGS } from '../..'; +import { CreateAggConfigParams, UI_SETTINGS, AggTypesDependencies } from '../..'; import { GetConfigFn } from '../../types'; import { AggConfigs, @@ -39,7 +39,7 @@ export interface AggsCommonSetupDependencies { export interface AggsCommonStartDependencies { getConfig: GetConfigFn; getIndexPattern(id: string): Promise; - isDefaultTimezone: () => boolean; + aggExecutionContext?: AggTypesDependencies['aggExecutionContext']; } /** @@ -67,14 +67,20 @@ export class AggsCommonService { }; } - public start({ getConfig }: AggsCommonStartDependencies): AggsCommonStart { + public start({ getConfig, aggExecutionContext }: AggsCommonStartDependencies): AggsCommonStart { const aggTypesStart = this.aggTypesRegistry.start(); const calculateAutoTimeExpression = getCalculateAutoTimeExpression(getConfig); const createAggConfigs = (indexPattern: DataView, configStates?: CreateAggConfigParams[]) => { - return new AggConfigs(indexPattern, configStates, { - typesRegistry: aggTypesStart, - }); + return new AggConfigs( + indexPattern, + configStates, + { + typesRegistry: aggTypesStart, + aggExecutionContext, + }, + getConfig + ); }; return { diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_order_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_order_helper.ts index bfe3c21653745..23d687a03d195 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_order_helper.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_order_helper.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import { IBucketAggConfig, BucketAggParam } from './bucket_agg_type'; export const termsAggFilter = [ @@ -77,10 +77,12 @@ export const termsOrderAggParamDefinition: Partial { const typesRegistry = mockAggTypesRegistry(); const getAggConfigs = (aggs: CreateAggConfigParams[] = []) => { - return new AggConfigs(indexPattern, [...aggs], { typesRegistry }); + return new AggConfigs(indexPattern, [...aggs], { typesRegistry }, jest.fn()); }; describe('buildOtherBucketAgg', () => { diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts index 972c5e5fcf44b..69db129bf1112 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import { createFilterDateHistogram } from './date_histogram'; import { intervalOptions, autoInterval } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; @@ -49,12 +49,13 @@ describe('AggConfig Filters', () => { ], { typesRegistry: mockAggTypesRegistry(), - } + }, + jest.fn() ); const bucketKey = 1422579600000; agg = aggConfigs.aggs[0] as IBucketDateHistogramAggConfig; - bucketStart = moment(bucketKey); + bucketStart = moment.tz(bucketKey, aggConfigs.timeZone); const timePad = moment.duration(duration / 2); diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts index 1fd2250ec9e8b..cd5b1644378bb 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import { buildRangeFilter } from '@kbn/es-query'; import { IBucketDateHistogramAggConfig } from '../date_histogram'; @@ -14,7 +14,7 @@ export const createFilterDateHistogram = ( agg: IBucketDateHistogramAggConfig, key: string | number ) => { - const start = moment(key); + const start = moment.tz(key, agg.aggConfigs.timeZone); const interval = agg.buckets.getInterval(); return buildRangeFilter( diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.test.ts index 02a1dec36e88e..6b2085abdeece 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import { createFilterDateRange } from './date_range'; import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; @@ -44,7 +44,8 @@ describe('AggConfig Filters', () => { ], { typesRegistry: mockAggTypesRegistry(), - } + }, + jest.fn() ); }; @@ -61,8 +62,16 @@ describe('AggConfig Filters', () => { expect(filter).toHaveProperty('meta'); expect(filter.meta).toHaveProperty('index', '1234'); expect(filter.query.range).toHaveProperty('@timestamp'); - expect(filter.query.range['@timestamp']).toHaveProperty('gte', moment(from).toISOString()); - expect(filter.query.range['@timestamp']).toHaveProperty('lt', moment(to).toISOString()); + + expect(filter.query.range['@timestamp']).toHaveProperty( + 'gte', + moment.tz(from, aggConfigs.timeZone).toISOString() + ); + + expect(filter.query.range['@timestamp']).toHaveProperty( + 'lt', + moment.tz(to, aggConfigs.timeZone).toISOString() + ); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts index c6062a26e48b9..e598761ec49e2 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_range.ts @@ -6,15 +6,16 @@ * Side Public License, v 1. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import { buildRangeFilter, RangeFilterParams } from '@kbn/es-query'; import { DateRange } from '../../../expressions'; import { IBucketAggConfig } from '../bucket_agg_type'; export const createFilterDateRange = (agg: IBucketAggConfig, { from, to }: DateRange) => { const filter: RangeFilterParams = {}; - if (from) filter.gte = moment(from).toISOString(); - if (to) filter.lt = moment(to).toISOString(); + if (from) filter.gte = moment.tz(from, agg.aggConfigs.timeZone).toISOString(); + if (to) filter.lt = moment.tz(to, agg.aggConfigs.timeZone).toISOString(); + if (to && from) filter.format = 'strict_date_optional_time'; return buildRangeFilter(agg.params.field, filter, agg.getIndexPattern()); diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/filters.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/filters.test.ts index 7115120ce01b6..826c210f902e1 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/filters.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/filters.test.ts @@ -44,7 +44,8 @@ describe('AggConfig Filters', () => { ], { typesRegistry: mockAggTypesRegistry(), - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts index f0dc3ad0cb7b3..0e632f3d98d99 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts @@ -58,7 +58,8 @@ describe('AggConfig Filters', () => { }, }, ], - { typesRegistry: mockAggTypesRegistry() } + { typesRegistry: mockAggTypesRegistry() }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.test.ts index f52388478df9b..ec81ca03ee6de 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/ip_range.test.ts @@ -32,7 +32,7 @@ describe('AggConfig Filters', () => { }, } as any; - return new AggConfigs(indexPattern, aggs, { typesRegistry }); + return new AggConfigs(indexPattern, aggs, { typesRegistry }, jest.fn()); }; test('should return a range filter for ip_range agg', () => { diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts index 873b6eb2e104f..e86a98ddbca67 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts @@ -47,7 +47,8 @@ describe('AggConfig Filters', () => { ], { typesRegistry: mockAggTypesRegistry(), - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.test.ts index af7a571f283a8..be51e674adfa5 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.test.ts @@ -30,9 +30,14 @@ describe('AggConfig Filters', () => { indexPattern, }; - return new AggConfigs(indexPattern, aggs, { - typesRegistry: mockAggTypesRegistry(), - }); + return new AggConfigs( + indexPattern, + aggs, + { + typesRegistry: mockAggTypesRegistry(), + }, + jest.fn() + ); }; test('should return a match_phrase filter for terms', () => { diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index 1d776bf643e1b..d72a0a3f22922 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -9,9 +9,15 @@ import { get, noop, find, every, omitBy, isNil } from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; +import { DataViewFieldBase } from '@kbn/es-query'; -import { KBN_FIELD_TYPES, TimeRange, TimeRangeBounds, UI_SETTINGS } from '../../..'; -import { IFieldType } from '../../..'; +import { + AggTypesDependencies, + KBN_FIELD_TYPES, + TimeRange, + TimeRangeBounds, + UI_SETTINGS, +} from '../../..'; import { ExtendedBounds, extendedBoundsToAst, timerangeToAst } from '../../expressions'; import { intervalOptions, autoInterval, isAutoInterval } from './_interval_options'; @@ -44,12 +50,6 @@ const updateTimeBuckets = ( buckets.setInterval(agg.params.interval); }; -export interface DateHistogramBucketAggDependencies { - calculateBounds: CalculateBoundsFn; - isDefaultTimezone: () => boolean; - getConfig: (key: string) => T; -} - export interface IBucketDateHistogramAggConfig extends IBucketAggConfig { buckets: TimeBuckets; } @@ -59,7 +59,7 @@ export function isDateHistogramBucketAggConfig(agg: any): agg is IBucketDateHist } export interface AggParamsDateHistogram extends BaseAggParams { - field?: IFieldType | string; + field?: DataViewFieldBase | string; timeRange?: TimeRange; useNormalizedEsInterval?: boolean; scaleMetricValues?: boolean; @@ -76,9 +76,9 @@ export interface AggParamsDateHistogram extends BaseAggParams { export const getDateHistogramBucketAgg = ({ calculateBounds, - isDefaultTimezone, + aggExecutionContext, getConfig, -}: DateHistogramBucketAggDependencies) => +}: AggTypesDependencies) => new BucketAggType({ name: BUCKET_TYPES.DATE_HISTOGRAM, expressionName: aggDateHistogramFnName, @@ -137,7 +137,15 @@ export const getDateHistogramBucketAgg = ({ }; }, getShiftedKey(agg, key, timeShift) { - return moment(key).add(timeShift).valueOf(); + const tz = inferTimeZone( + agg.params, + agg.getIndexPattern(), + 'date_histogram', + getConfig, + aggExecutionContext + ); + + return moment.tz(key, tz).add(timeShift).valueOf(); }, splitForTimeShift(agg, aggs) { return aggs.hasTimeShifts() && Boolean(aggs.timeFields?.includes(agg.fieldName())); @@ -263,7 +271,13 @@ export const getDateHistogramBucketAgg = ({ // time_zones being persisted into saved_objects serialize: noop, write(agg, output) { - const tz = inferTimeZone(agg.params, agg.getIndexPattern(), isDefaultTimezone, getConfig); + const tz = inferTimeZone( + agg.params, + agg.getIndexPattern(), + 'date_histogram', + getConfig, + aggExecutionContext + ); output.params.time_zone = tz; }, }, @@ -275,7 +289,13 @@ export const getDateHistogramBucketAgg = ({ write: () => {}, serialize(val, agg) { if (!agg) return undefined; - return inferTimeZone(agg.params, agg.getIndexPattern(), isDefaultTimezone, getConfig); + return inferTimeZone( + agg.params, + agg.getIndexPattern(), + 'date_histogram', + getConfig, + aggExecutionContext + ); }, toExpressionAst: () => undefined, }, @@ -300,11 +320,18 @@ export const getDateHistogramBucketAgg = ({ default: {}, write(agg, output) { const val = agg.params.extended_bounds; + const tz = inferTimeZone( + agg.params, + agg.getIndexPattern(), + 'date_histogram', + getConfig, + aggExecutionContext + ); if (val.min != null || val.max != null) { output.params.extended_bounds = { - min: moment(val.min).valueOf(), - max: moment(val.max).valueOf(), + min: moment.tz(val.min, tz).valueOf(), + max: moment.tz(val.max, tz).valueOf(), }; return; diff --git a/src/plugins/data/common/search/aggs/buckets/date_range.test.ts b/src/plugins/data/common/search/aggs/buckets/date_range.test.ts index 16a9e24513164..dda25408fc554 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range.test.ts @@ -17,8 +17,7 @@ describe('date_range params', () => { beforeEach(() => { aggTypesDependencies = { ...mockAggTypesDependencies, - getConfig: jest.fn(), - isDefaultTimezone: jest.fn().mockReturnValue(false), + getConfig: jest.fn().mockReturnValue('kibanaTimeZone'), }; }); @@ -59,7 +58,8 @@ describe('date_range params', () => { ], { typesRegistry: mockAggTypesRegistry(aggTypesDependencies), - } + }, + jest.fn() ); }; @@ -142,11 +142,6 @@ describe('date_range params', () => { }); test('should use the Kibana time_zone if no parameter specified', () => { - aggTypesDependencies = { - ...aggTypesDependencies, - getConfig: () => 'kibanaTimeZone' as any, - }; - const aggConfigs = getAggConfigs( { field: 'bytes', diff --git a/src/plugins/data/common/search/aggs/buckets/date_range.ts b/src/plugins/data/common/search/aggs/buckets/date_range.ts index 8fb0bf2d4ccb5..513f3c96a09a6 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range.ts @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import { get } from 'lodash'; -import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; +import { inferTimeZone } from '../../..'; import { DateRange, dateRangeToAst } from '../../expressions'; import { BUCKET_TYPES } from './bucket_agg_types'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; @@ -17,27 +16,20 @@ import { createFilterDateRange } from './create_filter/date_range'; import { aggDateRangeFnName } from './date_range_fn'; import { KBN_FIELD_TYPES } from '../../../kbn_field_types/types'; -import { BaseAggParams } from '../types'; +import type { BaseAggParams } from '../types'; +import type { AggTypesDependencies } from '../agg_types'; const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', { defaultMessage: 'Date Range', }); -export interface DateRangeBucketAggDependencies { - isDefaultTimezone: () => boolean; - getConfig: (key: string) => T; -} - export interface AggParamsDateRange extends BaseAggParams { field?: string; ranges?: DateRange[]; time_zone?: string; } -export const getDateRangeBucketAgg = ({ - isDefaultTimezone, - getConfig, -}: DateRangeBucketAggDependencies) => +export const getDateRangeBucketAgg = ({ aggExecutionContext, getConfig }: AggTypesDependencies) => new BucketAggType({ name: BUCKET_TYPES.DATE_RANGE, expressionName: aggDateRangeFnName, @@ -82,24 +74,13 @@ export const getDateRangeBucketAgg = ({ // Implimentation method is the same as that of date_histogram serialize: () => undefined, write: (agg, output) => { - const field = agg.getParam('field'); - let tz = agg.getParam('time_zone'); - - if (!tz && field) { - tz = get(agg.getIndexPattern(), [ - 'typeMeta', - 'aggs', - 'date_range', - field.name, - 'time_zone', - ]); - } - if (!tz) { - const detectedTimezone = moment.tz.guess(); - const tzOffset = moment().format('Z'); - - tz = isDefaultTimezone() ? detectedTimezone || tzOffset : getConfig('dateFormat:tz'); - } + const tz = inferTimeZone( + agg.params, + agg.getIndexPattern(), + 'date_range', + getConfig, + aggExecutionContext + ); output.params.time_zone = tz; }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/filters.test.ts b/src/plugins/data/common/search/aggs/buckets/filters.test.ts index da9279232fda0..bd6c09958d849 100644 --- a/src/plugins/data/common/search/aggs/buckets/filters.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters.test.ts @@ -51,7 +51,8 @@ describe('Filters Agg', () => { ], { typesRegistry: mockAggTypesRegistry(aggTypesDependencies), - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts index ddc59135e6a7c..efdbc921290a3 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_hash.test.ts @@ -58,7 +58,8 @@ describe('Geohash Agg', () => { ], { typesRegistry: mockAggTypesRegistry(), - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index afeb8b47372bd..75ed17a981386 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -48,7 +48,8 @@ describe('Histogram Agg', () => { ], { typesRegistry: mockAggTypesRegistry(aggTypesDependencies), - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts b/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts index f207b46b16c70..ae4b810184c45 100644 --- a/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts @@ -65,7 +65,8 @@ describe('Multi Terms Agg', () => { type: BUCKET_TYPES.MULTI_TERMS, }, ], - { typesRegistry: mockAggTypesRegistry() } + { typesRegistry: mockAggTypesRegistry() }, + jest.fn() ); }; @@ -185,7 +186,8 @@ describe('Multi Terms Agg', () => { type: BUCKET_TYPES.MULTI_TERMS, }, ], - { typesRegistry: mockAggTypesRegistry() } + { typesRegistry: mockAggTypesRegistry() }, + jest.fn() ); const { [BUCKET_TYPES.MULTI_TERMS]: params } = aggConfigs.aggs[0].toDsl(); expect(params.order).toEqual({ 'test-orderAgg.50': 'desc' }); diff --git a/src/plugins/data/common/search/aggs/buckets/range.test.ts b/src/plugins/data/common/search/aggs/buckets/range.test.ts index 5c1a8e6ec2aad..fa7a040b06ecb 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.test.ts @@ -51,7 +51,8 @@ describe('Range Agg', () => { ], { typesRegistry: mockAggTypesRegistry(), - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/rare_terms.test.ts b/src/plugins/data/common/search/aggs/buckets/rare_terms.test.ts index ed2b09bdcf832..a1cbb12b2b00e 100644 --- a/src/plugins/data/common/search/aggs/buckets/rare_terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/rare_terms.test.ts @@ -64,7 +64,8 @@ describe('rare terms Agg', () => { type: BUCKET_TYPES.RARE_TERMS, }, ], - { typesRegistry: mockAggTypesRegistry() } + { typesRegistry: mockAggTypesRegistry() }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts index cbd77ff1dd2f9..bc0446fa80bfe 100644 --- a/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay.test.ts @@ -45,7 +45,8 @@ describe('Shard Delay Agg', () => { typesRegistry: { get: getShardDelayBucketAgg, } as any, - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts index b7a1460e5837a..f95bd17b3860f 100644 --- a/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/significant_terms.test.ts @@ -40,7 +40,8 @@ describe('Significant Terms Agg', () => { ], { typesRegistry: mockAggTypesRegistry(), - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/significant_text.test.ts b/src/plugins/data/common/search/aggs/buckets/significant_text.test.ts index 1f3b6d897c5b1..bad2b46d57776 100644 --- a/src/plugins/data/common/search/aggs/buckets/significant_text.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/significant_text.test.ts @@ -37,7 +37,8 @@ describe('Significant Text Agg', () => { ], { typesRegistry: mockAggTypesRegistry(), - } + }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index 6fe9065282585..a135cb17845c7 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -66,7 +66,8 @@ describe('Terms Agg', () => { type: BUCKET_TYPES.TERMS, }, ], - { typesRegistry: mockAggTypesRegistry() } + { typesRegistry: mockAggTypesRegistry() }, + jest.fn() ); }; @@ -285,7 +286,8 @@ describe('Terms Agg', () => { type: BUCKET_TYPES.TERMS, }, ], - { typesRegistry: mockAggTypesRegistry() } + { typesRegistry: mockAggTypesRegistry() }, + jest.fn() ); const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); diff --git a/src/plugins/data/common/search/aggs/index.test.ts b/src/plugins/data/common/search/aggs/index.test.ts index 1c1a5e06334c7..e401655dbc7dd 100644 --- a/src/plugins/data/common/search/aggs/index.test.ts +++ b/src/plugins/data/common/search/aggs/index.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { getAggTypes } from '.'; +import { AggTypesDependencies, getAggTypes } from '.'; import { mockGetFieldFormatsStart } from './test_helpers'; import { isBucketAggType } from './buckets/bucket_agg_type'; @@ -15,11 +15,10 @@ import { isMetricAggType } from './metrics/metric_agg_type'; describe('AggTypesComponent', () => { const aggTypes = getAggTypes(); const { buckets, metrics } = aggTypes; - const aggTypesDependencies = { + const aggTypesDependencies: AggTypesDependencies = { calculateBounds: jest.fn(), getConfig: jest.fn(), getFieldFormatsStart: mockGetFieldFormatsStart, - isDefaultTimezone: jest.fn().mockReturnValue(true), }; describe('bucket aggs', () => { diff --git a/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts b/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts index c86c5d89ff6a3..b752b6e24fbcb 100644 --- a/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/filtered_metric.test.ts @@ -52,7 +52,8 @@ describe('filtered metric agg type', () => { ], { typesRegistry, - } + }, + jest.fn() ); }); diff --git a/src/plugins/data/common/search/aggs/metrics/median.test.ts b/src/plugins/data/common/search/aggs/metrics/median.test.ts index 612bf22628eb1..9ae226828db08 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.test.ts @@ -41,7 +41,8 @@ describe('AggTypeMetricMedianProvider class', () => { ], { typesRegistry, - } + }, + jest.fn() ); }); @@ -132,7 +133,8 @@ describe('AggTypeMetricMedianProvider class', () => { ], { typesRegistry, - } + }, + jest.fn() ); expect(aggConfigs.toDsl()).toMatchSnapshot(); diff --git a/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts b/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts index f9c62eeac7b29..d15457c613a5b 100644 --- a/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts @@ -89,7 +89,8 @@ describe('parent pipeline aggs', function () { schema: 'metric', }, ], - { typesRegistry } + { typesRegistry }, + jest.fn() ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts index 5013f77f08881..28e3bdd951918 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentile_ranks.test.ts @@ -48,7 +48,8 @@ describe('AggTypesMetricsPercentileRanksProvider class', function () { }, }, ], - { typesRegistry } + { typesRegistry }, + jest.fn() ); }); diff --git a/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts index 17c49e2484a80..65552b2481844 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentiles.test.ts @@ -43,7 +43,8 @@ describe('AggTypesMetricsPercentilesProvider class', () => { }, }, ], - { typesRegistry } + { typesRegistry }, + jest.fn() ); }); diff --git a/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts b/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts index f2fa990d6c507..aa8f6f8d03400 100644 --- a/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts @@ -93,7 +93,8 @@ describe('sibling pipeline aggs', () => { }, }, ], - { typesRegistry } + { typesRegistry }, + jest.fn() ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts index 967e1b1f624aa..b720ac1089417 100644 --- a/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts @@ -42,7 +42,8 @@ describe('AggTypeMetricSinglePercentileProvider class', () => { ], { typesRegistry, - } + }, + jest.fn() ); }); @@ -142,7 +143,8 @@ describe('AggTypeMetricSinglePercentileProvider class', () => { ], { typesRegistry, - } + }, + jest.fn() ); expect(aggConfigs.toDsl()).toMatchSnapshot(); diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile_rank.test.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile_rank.test.ts index 030b45422e7da..0f22ebd56b7a0 100644 --- a/src/plugins/data/common/search/aggs/metrics/single_percentile_rank.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile_rank.test.ts @@ -42,7 +42,8 @@ describe('AggTypeMetricSinglePercentileRankProvider class', () => { ], { typesRegistry, - } + }, + jest.fn() ); }); @@ -142,7 +143,8 @@ describe('AggTypeMetricSinglePercentileRankProvider class', () => { ], { typesRegistry, - } + }, + jest.fn() ); expect(aggConfigs.toDsl().single_percentile_rank.percentile_ranks.script.source).toEqual( diff --git a/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts index 97f1e3474778e..38e0ca8c39756 100644 --- a/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/std_deviation.test.ts @@ -41,7 +41,8 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { }, }, ], - { typesRegistry } + { typesRegistry }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts index 69b1b5e44205d..719687ea970bd 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts @@ -70,7 +70,8 @@ describe('Top hit metric', () => { params, }, ], - { typesRegistry } + { typesRegistry }, + jest.fn() ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts index 6542c6f398cb9..7b34a6f4a704f 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_metrics.test.ts @@ -68,7 +68,8 @@ describe('Top metrics metric', () => { params, }, ], - { typesRegistry } + { typesRegistry }, + jest.fn() ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) diff --git a/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts index 663de94952bbd..578e66222e01b 100644 --- a/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts +++ b/src/plugins/data/common/search/aggs/test_helpers/mock_agg_types_registry.ts @@ -45,7 +45,6 @@ export const mockAggTypesDependencies: AggTypesDependencies = { calculateBounds: jest.fn(), getFieldFormatsStart: mockGetFieldFormatsStart, getConfig: mockGetConfig, - isDefaultTimezone: () => true, }; /** diff --git a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts index 61b61d70f1375..5e36fbb791e28 100644 --- a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts +++ b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts @@ -12,7 +12,6 @@ import { i18n } from '@kbn/i18n'; import { FieldFormat, FieldFormatInstanceType, - FieldFormatParams, FieldFormatsContentType, IFieldFormat, SerializedFieldFormat, @@ -129,13 +128,13 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta convert = (val: string, type: FieldFormatsContentType) => { const params = this._params; - const format = getFieldFormat({ id: params.id, params }); + const format = getFieldFormat({ id: `${params.id}`, params }); if (val === '__other__') { - return params.otherBucketLabel; + return `${params.otherBucketLabel}`; } if (val === '__missing__') { - return params.missingBucketLabel; + return `${params.missingBucketLabel}`; } return format.convert(val, type); @@ -146,7 +145,7 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta static id = 'multi_terms'; static hidden = true; - private formatCache: Map, FieldFormat> = new Map(); + private formatCache: Map = new Map(); convert = (val: unknown, type: FieldFormatsContentType) => { const params = this._params; @@ -160,10 +159,10 @@ export function getAggsFormats(getFieldFormat: GetFieldFormat): FieldFormatInsta }); if (String(val) === '__other__') { - return params.otherBucketLabel; + return `${params.otherBucketLabel}`; } - const joinTemplate = params.separator ?? ' › '; + const joinTemplate = `${params.separator ?? ' › '}`; return (val as MultiFieldKey).keys .map((valPart, i) => formats[i].convert(valPart, type)) diff --git a/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts b/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts index 13acff39ebaa4..1e625e75971b7 100644 --- a/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts +++ b/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts @@ -6,28 +6,17 @@ * Side Public License, v 1. */ -jest.mock('moment', () => { - const moment: any = jest.fn(() => { - return { - format: jest.fn(() => '-1;00'), - }; - }); - moment.tz = { - guess: jest.fn(() => 'CET'), - }; - return moment; -}); - import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; -import { AggParamsDateHistogram } from '../buckets'; import { inferTimeZone } from './infer_time_zone'; describe('inferTimeZone', () => { it('reads time zone from agg params', () => { - const params: AggParamsDateHistogram = { + const params = { time_zone: 'CEST', }; - expect(inferTimeZone(params, {} as DataView, () => false, jest.fn())).toEqual('CEST'); + expect( + inferTimeZone(params, {} as DataView, 'date_histogram', jest.fn().mockReturnValue('UTC')) + ).toEqual('CEST'); }); it('reads time zone from index pattern type meta if available', () => { @@ -45,8 +34,8 @@ describe('inferTimeZone', () => { }, }, } as unknown as DataView, - () => false, - jest.fn() + 'date_histogram', + jest.fn().mockReturnValue('CET') ) ).toEqual('UTC'); }); @@ -70,24 +59,15 @@ describe('inferTimeZone', () => { }, }, } as unknown as DataView, - () => false, - jest.fn() + 'date_histogram', + jest.fn().mockReturnValue('CET') ) ).toEqual('UTC'); }); - it('reads time zone from moment if set to default', () => { - expect(inferTimeZone({}, {} as DataView, () => true, jest.fn())).toEqual('CET'); - }); - it('reads time zone from config if not set to default', () => { expect( - inferTimeZone( - {}, - {} as DataView, - () => false, - () => 'CET' as any - ) + inferTimeZone({}, {} as DataView, 'date_histogram', jest.fn().mockReturnValue('CET')) ).toEqual('CET'); }); }); diff --git a/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts b/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts index c94f99b7eb928..ecda8380ac2d2 100644 --- a/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts +++ b/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts @@ -6,31 +6,30 @@ * Side Public License, v 1. */ -import moment from 'moment'; -import type { DataView } from '@kbn/data-views-plugin/common'; -import { AggParamsDateHistogram } from '../buckets'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import type { AggTypesDependencies } from '../../..'; +import { getUserTimeZone } from '../../utils'; export function inferTimeZone( - params: AggParamsDateHistogram, - indexPattern: DataView, - isDefaultTimezone: () => boolean, - getConfig: (key: string) => T + params: { field?: DataViewField | string; time_zone?: string }, + dataView: DataView, + aggName: 'date_histogram' | 'date_range', + getConfig: AggTypesDependencies['getConfig'], + { shouldDetectTimeZone }: AggTypesDependencies['aggExecutionContext'] = {} ) { let tz = params.time_zone; + if (!tz && params.field) { // If a field has been configured check the index pattern's typeMeta if a date_histogram on that // field requires a specific time_zone const fieldName = typeof params.field === 'string' ? params.field : params.field.name; - tz = indexPattern.typeMeta?.aggs?.date_histogram?.[fieldName]?.time_zone; + + tz = dataView.typeMeta?.aggs?.[aggName]?.[fieldName]?.time_zone; } + if (!tz) { - // If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz - const detectedTimezone = moment.tz.guess(); - const tzOffset = moment().format('Z'); - tz = isDefaultTimezone() - ? detectedTimezone || tzOffset - : // if timezone is not the default, this will always return a string - (getConfig('dateFormat:tz') as string); + return getUserTimeZone(getConfig, shouldDetectTimeZone); } + return tz; } diff --git a/src/plugins/data/common/search/aggs/utils/time_splits.ts b/src/plugins/data/common/search/aggs/utils/time_splits.ts index eb47181080dd3..c2fe8aaca0fb2 100644 --- a/src/plugins/data/common/search/aggs/utils/time_splits.ts +++ b/src/plugins/data/common/search/aggs/utils/time_splits.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import { isArray } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -412,7 +412,8 @@ export function insertTimeShiftSplit( aggConfigs: AggConfigs, config: AggConfig, timeShifts: Record, - dslLvlCursor: Record + dslLvlCursor: Record, + defaultTimeZone: string ) { if ('splitForTimeShift' in config.type && !config.type.splitForTimeShift(config, aggConfigs)) { return dslLvlCursor; @@ -436,8 +437,14 @@ export function insertTimeShiftSplit( range: { [timeField]: { format: 'strict_date_optional_time', - gte: moment(timeFilter.query.range[timeField].gte).subtract(shift).toISOString(), - lte: moment(timeFilter.query.range[timeField].lte).subtract(shift).toISOString(), + gte: moment + .tz(timeFilter.query.range[timeField].gte, defaultTimeZone) + .subtract(shift) + .toISOString(), + lte: moment + .tz(timeFilter.query.range[timeField].lte, defaultTimeZone) + .subtract(shift) + .toISOString(), }, }, }; diff --git a/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts b/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts index ad584ff8dd8bf..9bda74d324d2b 100644 --- a/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/create_filter.test.ts @@ -50,7 +50,8 @@ describe('createFilter', () => { params, }, ], - { typesRegistry } + { typesRegistry }, + jest.fn() ); }; diff --git a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts index 6ef043a33dcbe..7f03aacdb8c90 100644 --- a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts +++ b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts @@ -13,7 +13,7 @@ import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-p import { buildExpressionFunction } from '@kbn/expressions-plugin/common'; import { IndexPatternExpressionType } from '@kbn/data-views-plugin/common/expressions'; -import { IndexPatternsContract } from '../../..'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { AggsStart, AggExpressionType, aggCountFnName } from '../../aggs'; import { ISearchStartSearchSource } from '../../search_source'; @@ -44,7 +44,7 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition< /** @internal */ export interface EsaggsStartDependencies { aggs: AggsStart; - indexPatterns: IndexPatternsContract; + indexPatterns: DataViewsContract; searchSource: ISearchStartSearchSource; getNow?: () => Date; } diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts index 33b4d0cb5d51e..4f112c3f64206 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts @@ -7,7 +7,7 @@ */ import { from } from 'rxjs'; -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import type { Filter } from '../../../es_query'; import type { IAggConfigs } from '../../aggs'; import type { ISearchSource } from '../../search_source'; @@ -40,6 +40,7 @@ describe('esaggs expression function - public', () => { abortSignal: jest.fn() as unknown as jest.Mocked, aggs: { aggs: [{ type: { name: 'terms', postFlightRequest: jest.fn().mockResolvedValue({}) } }], + partialRows: false, setTimeRange: jest.fn(), toDsl: jest.fn().mockReturnValue({ aggs: {} }), onSearchRequestStart: jest.fn(), @@ -49,7 +50,6 @@ describe('esaggs expression function - public', () => { filters: undefined, indexPattern: { id: 'logstash-*' } as unknown as jest.Mocked, inspectorAdapters: {}, - partialRows: false, query: undefined, searchSessionId: 'abc123', searchSourceService: searchSourceCommonMock, @@ -147,7 +147,7 @@ describe('esaggs expression function - public', () => { mockParams.aggs, {}, { - partialRows: mockParams.partialRows, + partialRows: mockParams.aggs.partialRows, timeRange: mockParams.timeRange, } ); diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index 7d8aadf2cad3b..8caa93c4461ef 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -24,8 +24,6 @@ interface RequestHandlerParams { filters?: Filter[]; indexPattern?: DataView; inspectorAdapters: Adapters; - metricsAtAllLevels?: boolean; - partialRows?: boolean; query?: Query; searchSessionId?: string; searchSourceService: ISearchStartSearchSource; @@ -41,7 +39,6 @@ export const handleRequest = ({ filters, indexPattern, inspectorAdapters, - partialRows, query, searchSessionId, searchSourceService, @@ -131,7 +128,7 @@ export const handleRequest = ({ const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null; const tabifyParams = { metricsAtAllLevels: aggs.hierarchical, - partialRows, + partialRows: aggs.partialRows, timeRange: parsedTimeRange ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } : undefined, diff --git a/src/plugins/data/common/search/expressions/kibana_context.test.ts b/src/plugins/data/common/search/expressions/kibana_context.test.ts index 3e07366b76c2d..4ef24bdc3fe3d 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.test.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.test.ts @@ -7,7 +7,7 @@ */ import { FilterStateStore, buildFilter, FILTERS } from '@kbn/es-query'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { ExecutionContext } from '@kbn/expressions-plugin/common'; import { KibanaContext } from './kibana_context_type'; @@ -89,16 +89,28 @@ describe('kibanaContextFn', () => { } as any); const args = { ...emptyArgs, - q: { - type: 'kibana_query' as 'kibana_query', - language: 'test', - query: { - type: 'test', - match_phrase: { - test: 'something2', + q: [ + { + type: 'kibana_query' as 'kibana_query', + language: 'test', + query: { + type: 'test', + match_phrase: { + test: 'something2', + }, }, }, - }, + { + type: 'kibana_query' as 'kibana_query', + language: 'test', + query: { + type: 'test', + match_phrase: { + test: 'something3', + }, + }, + }, + ], savedSearchId: 'test', }; const input: KibanaContext = { @@ -183,6 +195,16 @@ describe('kibanaContextFn', () => { }, }, }, + { + type: 'kibana_query', + language: 'test', + query: { + type: 'test', + match_phrase: { + test: 'something3', + }, + }, + }, { language: 'kuery', query: { diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index c95e7e99017c0..6183484a57b46 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -14,17 +14,17 @@ import { Filter } from '@kbn/es-query'; import { Query, uniqFilters } from '@kbn/es-query'; import { unboxExpressionValue } from '@kbn/expressions-plugin/common'; import { SavedObjectReference } from '@kbn/core/types'; +import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/common'; import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type'; import { KibanaQueryOutput } from './kibana_context_type'; import { KibanaTimerangeOutput } from './timerange'; -import { SavedObjectsClientCommon } from '../..'; export interface KibanaContextStartDependencies { savedObjectsClient: SavedObjectsClientCommon; } interface Arguments { - q?: KibanaQueryOutput | null; + q?: KibanaQueryOutput[] | null; filters?: KibanaFilter[] | null; timeRange?: KibanaTimerangeOutput | null; savedSearchId?: string | null; @@ -62,8 +62,8 @@ export const getKibanaContextFn = ( args: { q: { types: ['kibana_query', 'null'], + multi: true, aliases: ['query', '_'], - default: null, help: i18n.translate('data.search.functions.kibana_context.q.help', { defaultMessage: 'Specify Kibana free form text query', }), @@ -123,7 +123,7 @@ export const getKibanaContextFn = ( const { savedObjectsClient } = await getStartDependencies(getKibanaRequest); const timeRange = args.timeRange || input?.timeRange; - let queries = mergeQueries(input?.query, args?.q || []); + let queries = mergeQueries(input?.query, args?.q?.filter(Boolean) || []); let filters = [ ...(input?.filters || []), ...((args?.filters?.map(unboxExpressionValue) || []) as Filter[]), diff --git a/src/plugins/data/common/search/search_source/create_search_source.test.ts b/src/plugins/data/common/search/search_source/create_search_source.test.ts index 7ce3d02763137..c67e8a21b4f9a 100644 --- a/src/plugins/data/common/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/common/search/search_source/create_search_source.test.ts @@ -8,18 +8,18 @@ import { createSearchSource as createSearchSourceFactory } from './create_search_source'; import { SearchSourceDependencies } from './search_source'; -import type { DataView } from '@kbn/data-views-plugin/common'; -import { IndexPatternsContract } from '../..'; +import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/common'; import { Filter } from '../../es_query'; describe('createSearchSource', () => { const indexPatternMock: DataView = {} as DataView; - let indexPatternContractMock: jest.Mocked; + let indexPatternContractMock: jest.Mocked; let dependencies: SearchSourceDependencies; let createSearchSource: ReturnType; beforeEach(() => { dependencies = { + aggs: {} as SearchSourceDependencies['aggs'], getConfig: jest.fn(), search: jest.fn(), onResponse: (req, res) => res, @@ -27,7 +27,7 @@ describe('createSearchSource', () => { indexPatternContractMock = { get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)), - } as unknown as jest.Mocked; + } as unknown as jest.Mocked; createSearchSource = createSearchSourceFactory(indexPatternContractMock, dependencies); }); diff --git a/src/plugins/data/common/search/search_source/create_search_source.ts b/src/plugins/data/common/search/search_source/create_search_source.ts index 3d2300940ac06..c6093da07b8c2 100644 --- a/src/plugins/data/common/search/search_source/create_search_source.ts +++ b/src/plugins/data/common/search/search_source/create_search_source.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { migrateLegacyQuery } from './migrate_legacy_query'; import { SearchSource, SearchSourceDependencies } from './search_source'; -import { IndexPatternsContract, SerializedSearchSourceFields } from '../..'; +import { SerializedSearchSourceFields } from '../..'; import { SearchSourceFields } from './types'; /** @@ -29,7 +30,7 @@ import { SearchSourceFields } from './types'; * * @public */ export const createSearchSource = ( - indexPatterns: IndexPatternsContract, + indexPatterns: DataViewsContract, searchSourceDependencies: SearchSourceDependencies ) => { const createFields = async (searchSourceFields: SerializedSearchSourceFields = {}) => { diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index eb10855460236..e291a9ec27cff 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -7,10 +7,10 @@ */ import { of } from 'rxjs'; -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { uiSettingsServiceMock } from '@kbn/core/public/mocks'; -import { SearchSource } from './search_source'; +import { SearchSource, SearchSourceDependencies } from './search_source'; import { ISearchStartSearchSource, ISearchSource, SearchSourceFields } from './types'; export const searchSourceInstanceMock: MockedKeys = { @@ -35,6 +35,7 @@ export const searchSourceInstanceMock: MockedKeys = { history: [], getSerializedFields: jest.fn(), serialize: jest.fn(), + toExpressionAst: jest.fn(), }; export const searchSourceCommonMock: jest.Mocked = { @@ -48,6 +49,9 @@ export const searchSourceCommonMock: jest.Mocked = { export const createSearchSourceMock = (fields?: SearchSourceFields, response?: any) => new SearchSource(fields, { + aggs: { + createAggConfigs: jest.fn(), + } as unknown as SearchSourceDependencies['aggs'], getConfig: uiSettingsServiceMock.createStartContract().get, search: jest.fn().mockReturnValue( of( diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 55195902c2e98..37b5545204569 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -8,12 +8,15 @@ import { lastValueFrom, of, throwError } from 'rxjs'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { buildExpression, ExpressionAstExpression } from '@kbn/expressions-plugin/common'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { SearchSource, SearchSourceDependencies, SortDirection } from '.'; import { AggConfigs, AggTypesRegistryStart } from '../..'; import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { RequestResponder } from '@kbn/inspector-plugin/common'; import { switchMap } from 'rxjs/operators'; import { Filter } from '@kbn/es-query'; +import { stubIndexPattern } from '../../stubs'; const getComputedFields = () => ({ storedFields: [], @@ -26,6 +29,7 @@ const mockSource = { excludes: ['foo-*'] }; const mockSource2 = { excludes: ['bar-*'] }; const indexPattern = { + id: '1234', title: 'foo', fields: [{ name: 'foo-bar' }, { name: 'field1' }, { name: 'field2' }, { name: '_id' }], getComputedFields, @@ -62,10 +66,13 @@ const runtimeFieldDef = { describe('SearchSource', () => { let mockSearchMethod: any; - let searchSourceDependencies: SearchSourceDependencies; + let searchSourceDependencies: MockedKeys; let searchSource: SearchSource; beforeEach(() => { + const aggsMock = { + createAggConfigs: jest.fn(), + } as unknown as jest.Mocked; const getConfigMock = jest .fn() .mockImplementation((param) => param === 'metaFields' && ['_type', '_source', '_id']) @@ -81,6 +88,7 @@ describe('SearchSource', () => { ); searchSourceDependencies = { + aggs: aggsMock, getConfig: getConfigMock, search: mockSearchMethod, onResponse: (req, res) => res, @@ -127,9 +135,14 @@ describe('SearchSource', () => { test('sets the value for the property with AggConfigs', () => { const typesRegistry = mockAggTypesRegistry(); - const ac = new AggConfigs(indexPattern3, [{ type: 'avg', params: { field: 'field1' } }], { - typesRegistry, - }); + const ac = new AggConfigs( + indexPattern3, + [{ type: 'avg', params: { field: 'field1' } }], + { + typesRegistry, + }, + jest.fn() + ); searchSource.setField('aggs', ac); const request = searchSource.getSearchRequestBody(); @@ -1146,7 +1159,8 @@ describe('SearchSource', () => { ], { typesRegistry, - } + }, + jest.fn() ); } @@ -1231,7 +1245,8 @@ describe('SearchSource', () => { ], { typesRegistry, - } + }, + jest.fn() ); searchSource = new SearchSource({}, searchSourceDependencies); @@ -1266,7 +1281,8 @@ describe('SearchSource', () => { ], { typesRegistry, - } + }, + jest.fn() ); searchSource = new SearchSource({}, searchSourceDependencies); @@ -1309,4 +1325,146 @@ describe('SearchSource', () => { }); }); }); + + describe('#toExpressionAst()', () => { + function toString(ast: ExpressionAstExpression) { + return buildExpression(ast).toString(); + } + + test('should generate an expression AST', () => { + expect(toString(searchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context + | esdsl dsl=\\"{}\\"" + `); + }); + + test('should generate query argument', () => { + searchSource.setField('query', { language: 'kuery', query: 'something' }); + + expect(toString(searchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context q={kql q=\\"something\\"} + | esdsl dsl=\\"{}\\"" + `); + }); + + test('should generate filters argument', () => { + const filter1 = { + query: { query_string: { query: 'query1' } }, + meta: {}, + }; + const filter2 = { + query: { query_string: { query: 'query2' } }, + meta: {}, + }; + searchSource.setField('filter', [filter1, filter2]); + + expect(toString(searchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context filters={kibanaFilter query=\\"{\\\\\\"query_string\\\\\\":{\\\\\\"query\\\\\\":\\\\\\"query1\\\\\\"}}\\"} + filters={kibanaFilter query=\\"{\\\\\\"query_string\\\\\\":{\\\\\\"query\\\\\\":\\\\\\"query2\\\\\\"}}\\"} + | esdsl dsl=\\"{}\\"" + `); + }); + + test('should resolve filters if set as a function', () => { + const filter = { + query: { query_string: { query: 'query' } }, + meta: {}, + }; + searchSource.setField('filter', () => filter); + + expect(toString(searchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context filters={kibanaFilter query=\\"{\\\\\\"query_string\\\\\\":{\\\\\\"query\\\\\\":\\\\\\"query\\\\\\"}}\\"} + | esdsl dsl=\\"{}\\"" + `); + }); + + test('should merge properties from parent search sources', () => { + const filter1 = { + query: { query_string: { query: 'query1' } }, + meta: {}, + }; + const filter2 = { + query: { query_string: { query: 'query2' } }, + meta: {}, + }; + searchSource.setField('query', { language: 'kuery', query: 'something1' }); + searchSource.setField('filter', filter1); + + const childSearchSource = searchSource.createChild(); + childSearchSource.setField('query', { language: 'kuery', query: 'something2' }); + childSearchSource.setField('filter', filter2); + + expect(toString(childSearchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context q={kql q=\\"something2\\"} q={kql q=\\"something1\\"} filters={kibanaFilter query=\\"{\\\\\\"query_string\\\\\\":{\\\\\\"query\\\\\\":\\\\\\"query2\\\\\\"}}\\"} + filters={kibanaFilter query=\\"{\\\\\\"query_string\\\\\\":{\\\\\\"query\\\\\\":\\\\\\"query1\\\\\\"}}\\"} + | esdsl dsl=\\"{}\\"" + `); + }); + + test('should include a data view identifier', () => { + searchSource.setField('index', indexPattern); + + expect(toString(searchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context + | esdsl dsl=\\"{}\\" index=\\"1234\\"" + `); + }); + + test('should include size if present', () => { + searchSource.setField('size', 1000); + + expect(toString(searchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context + | esdsl size=1000 dsl=\\"{}\\"" + `); + }); + + test('should generate the `esaggs` function if there are aggregations', () => { + const typesRegistry = mockAggTypesRegistry(); + const aggConfigs = new AggConfigs( + stubIndexPattern, + [{ enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } }], + { typesRegistry }, + jest.fn() + ); + searchSource.setField('aggs', aggConfigs); + + expect(toString(searchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context + | esaggs index={indexPatternLoad id=\\"logstash-*\\"} metricsAtAllLevels=false partialRows=false aggs={aggAvg field=\\"bytes\\" id=\\"1\\" enabled=true schema=\\"metric\\"}" + `); + }); + + test('should generate the `esaggs` function if there are aggregations configs', () => { + const typesRegistry = mockAggTypesRegistry(); + searchSourceDependencies.aggs.createAggConfigs.mockImplementationOnce( + (dataView, configs) => new AggConfigs(dataView, configs, { typesRegistry }, jest.fn()) + ); + searchSource.setField('index', stubIndexPattern); + searchSource.setField('aggs', [ + { enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + ]); + + expect(toString(searchSource.toExpressionAst())).toMatchInlineSnapshot(` + "kibana_context + | esaggs index={indexPatternLoad id=\\"logstash-*\\"} metricsAtAllLevels=false partialRows=false aggs={aggAvg field=\\"bytes\\" id=\\"1\\" enabled=true schema=\\"metric\\"}" + `); + }); + + test('should not include the `esdsl` function to the chain if the `asDatatable` option is false', () => { + expect(toString(searchSource.toExpressionAst({ asDatatable: false }))).toMatchInlineSnapshot( + `"kibana_context"` + ); + }); + + test('should not include the `esaggs` function to the chain if the `asDatatable` option is false', () => { + searchSource.setField('aggs', [ + { enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + ]); + + expect(toString(searchSource.toExpressionAst({ asDatatable: false }))).toMatchInlineSnapshot( + `"kibana_context"` + ); + }); + }); }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index d3829781b6512..cd22127ca2fd7 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -76,6 +76,11 @@ import { buildEsQuery, Filter } from '@kbn/es-query'; import { fieldWildcardFilter } from '@kbn/kibana-utils-plugin/common'; import { getHighlightRequest } from '@kbn/field-formats-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { + ExpressionAstExpression, + buildExpression, + buildExpressionFunction, +} from '@kbn/expressions-plugin/common'; import { normalizeSortRequest } from './normalize_sort_request'; import { AggConfigSerialized, DataViewField, SerializedSearchSourceFields } from '../..'; @@ -104,7 +109,14 @@ import { isPartialResponse, UI_SETTINGS, } from '../..'; +import { AggsStart } from '../aggs'; import { extractReferences } from './extract_references'; +import { + EsdslExpressionFunctionDefinition, + ExpressionFunctionKibanaContext, + filtersToAst, + queryToAst, +} from '../expressions'; /** @internal */ export const searchSourceRequiredUiSettings = [ @@ -122,9 +134,19 @@ export const searchSourceRequiredUiSettings = [ ]; export interface SearchSourceDependencies extends FetchHandlers { + aggs: AggsStart; search: ISearchGeneric; } +interface ExpressionAstOptions { + /** + * When truthy, it will include either `esaggs` or `esdsl` function to the expression chain. + * In this case, the expression will perform a search and return the `datatable` structure. + * @default true + */ + asDatatable?: boolean; +} + /** @public **/ export class SearchSource { private id: string = uniqueId('data_source'); @@ -922,4 +944,54 @@ export class SearchSource { return [filterField]; } + + /** + * Generates an expression abstract syntax tree using the fields set in the current search source and its ancestors. + * The produced expression from the returned AST will return the `datatable` structure. + * If the `asDatatable` option is truthy or omitted, the generator will use the `esdsl` function to perform the search. + * When the `aggs` field is present, it will use the `esaggs` function instead. + * @returns The expression AST. + */ + toExpressionAst({ asDatatable = true }: ExpressionAstOptions = {}): ExpressionAstExpression { + const searchRequest = this.mergeProps(); + const { body, index, query } = searchRequest; + + const filters = ( + typeof searchRequest.filters === 'function' ? searchRequest.filters() : searchRequest.filters + ) as Filter[] | Filter | undefined; + const ast = buildExpression([ + buildExpressionFunction('kibana_context', { + q: query?.map(queryToAst), + filters: filters && filtersToAst(filters), + }), + ]).toAst(); + + if (!asDatatable) { + return ast; + } + + const aggsField = this.getField('aggs'); + const aggs = (typeof aggsField === 'function' ? aggsField() : aggsField) as + | AggConfigs + | AggConfigSerialized[] + | undefined; + const aggConfigs = + aggs instanceof AggConfigs + ? aggs + : index && aggs && this.dependencies.aggs.createAggConfigs(index, aggs); + + if (aggConfigs) { + ast.chain.push(...aggConfigs.toExpressionAst().chain); + } else { + ast.chain.push( + buildExpressionFunction('esdsl', { + size: body?.size, + dsl: JSON.stringify({}), + index: index?.id, + }).toAst() + ); + } + + return ast; + } } diff --git a/src/plugins/data/common/search/search_source/search_source_service.test.ts b/src/plugins/data/common/search/search_source/search_source_service.test.ts index d6b06bb22288e..70448db335a07 100644 --- a/src/plugins/data/common/search/search_source/search_source_service.test.ts +++ b/src/plugins/data/common/search/search_source/search_source_service.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternsContract } from '../..'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { SearchSourceService, SearchSourceDependencies } from '.'; describe('SearchSource service', () => { @@ -15,6 +15,7 @@ describe('SearchSource service', () => { beforeEach(() => { jest.resetModules(); dependencies = { + aggs: {} as SearchSourceDependencies['aggs'], getConfig: jest.fn(), search: jest.fn(), onResponse: jest.fn(), @@ -24,7 +25,7 @@ describe('SearchSource service', () => { describe('start()', () => { test('exposes proper contract', () => { const start = new SearchSourceService().start( - jest.fn() as unknown as jest.Mocked, + jest.fn() as unknown as jest.Mocked, dependencies ); diff --git a/src/plugins/data/common/search/search_source/search_source_service.ts b/src/plugins/data/common/search/search_source/search_source_service.ts index e87f589044f9a..bf2f119f27e75 100644 --- a/src/plugins/data/common/search/search_source/search_source_service.ts +++ b/src/plugins/data/common/search/search_source/search_source_service.ts @@ -11,6 +11,7 @@ import { mergeMigrationFunctionMaps, MigrateFunctionsObject, } from '@kbn/kibana-utils-plugin/common'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { createSearchSource, extractReferences, @@ -19,7 +20,6 @@ import { SearchSourceDependencies, SerializedSearchSourceFields, } from '.'; -import { IndexPatternsContract } from '../..'; import { getAllMigrations as filtersGetAllMigrations } from '../../query/filters/persistable_state'; const getAllMigrations = (): MigrateFunctionsObject => { @@ -42,7 +42,7 @@ export class SearchSourceService { return { getAllMigrations }; } - public start(indexPatterns: IndexPatternsContract, dependencies: SearchSourceDependencies) { + public start(indexPatterns: DataViewsContract, dependencies: SearchSourceDependencies) { return { /** * creates searchsource based on serialized search source fields diff --git a/src/plugins/data/common/search/tabify/buckets.test.ts b/src/plugins/data/common/search/tabify/buckets.test.ts index 615a3d160c48d..57c65174c45a8 100644 --- a/src/plugins/data/common/search/tabify/buckets.test.ts +++ b/src/plugins/data/common/search/tabify/buckets.test.ts @@ -71,6 +71,7 @@ describe('Buckets wrapper', () => { }, ], }, + aggConfigs: {}, } as IAggConfig; const buckets = new TabifyBuckets(aggResp, agg); @@ -103,6 +104,7 @@ describe('Buckets wrapper', () => { }, ], }, + aggConfigs: {}, } as IAggConfig; const buckets = new TabifyBuckets(aggResp, agg); @@ -130,6 +132,7 @@ describe('Buckets wrapper', () => { }, ], }, + aggConfigs: {}, } as IAggConfig; const buckets = new TabifyBuckets(aggResp, agg); @@ -187,6 +190,7 @@ describe('Buckets wrapper', () => { name: 'date', }, }, + aggConfigs: {}, } as IAggConfig; const timeRange = { from: moment(150), @@ -211,6 +215,7 @@ describe('Buckets wrapper', () => { serialize: () => ({ params: { used_interval: '100ms' }, }), + aggConfigs: {}, } as unknown as IAggConfig; const timeRange = { from: moment(1050), @@ -245,6 +250,7 @@ describe('Buckets wrapper', () => { name: 'date', }, }, + aggConfigs: {}, } as IAggConfig; const timeRange = { from: moment(150), @@ -264,6 +270,7 @@ describe('Buckets wrapper', () => { name: 'date', }, }, + aggConfigs: {}, } as IAggConfig; const timeRange = { from: moment(100), @@ -283,6 +290,7 @@ describe('Buckets wrapper', () => { name: 'other_time', }, }, + aggConfigs: {}, } as IAggConfig; const timeRange = { from: moment(150), @@ -302,6 +310,7 @@ describe('Buckets wrapper', () => { name: 'date', }, }, + aggConfigs: {}, } as IAggConfig; const timeRange = { from: moment(100), @@ -321,6 +330,7 @@ describe('Buckets wrapper', () => { name: 'date', }, }, + aggConfigs: {}, } as IAggConfig; const timeRange = { from: moment(100), diff --git a/src/plugins/data/common/search/tabify/buckets.ts b/src/plugins/data/common/search/tabify/buckets.ts index ba2190a034e60..7835d373c656d 100644 --- a/src/plugins/data/common/search/tabify/buckets.ts +++ b/src/plugins/data/common/search/tabify/buckets.ts @@ -7,7 +7,7 @@ */ import { get, isPlainObject, keys, findKey } from 'lodash'; -import moment from 'moment'; +import moment from 'moment-timezone'; import { IAggConfig, parseInterval } from '../aggs'; import { AggResponseBucket, TabbedRangeFilterParams, TimeRangeInformation } from './types'; @@ -112,10 +112,10 @@ export class TabifyBuckets { : moment.duration(this.buckets[1].key - this.buckets[0].key); this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { - if (moment(bucket.key).isBefore(timeRange.from)) { + if (moment.tz(bucket.key, agg.aggConfigs.timeZone).isBefore(timeRange.from)) { return false; } - if (moment(bucket.key).add(interval).isAfter(timeRange.to)) { + if (moment.tz(bucket.key, agg.aggConfigs.timeZone).add(interval).isAfter(timeRange.to)) { return false; } return true; diff --git a/src/plugins/data/common/search/tabify/get_columns.test.ts b/src/plugins/data/common/search/tabify/get_columns.test.ts index 1741abfe729d7..42c8699712116 100644 --- a/src/plugins/data/common/search/tabify/get_columns.test.ts +++ b/src/plugins/data/common/search/tabify/get_columns.test.ts @@ -33,7 +33,7 @@ describe('get columns', () => { }, } as any; - return new AggConfigs(indexPattern, aggs, { typesRegistry }); + return new AggConfigs(indexPattern, aggs, { typesRegistry }, jest.fn()); }; test('should inject the metric after each bucket if the vis is hierarchical', () => { diff --git a/src/plugins/data/common/search/tabify/response_writer.test.ts b/src/plugins/data/common/search/tabify/response_writer.test.ts index 85f815447619a..e16dae23a7ebc 100644 --- a/src/plugins/data/common/search/tabify/response_writer.test.ts +++ b/src/plugins/data/common/search/tabify/response_writer.test.ts @@ -72,11 +72,14 @@ describe('TabbedAggResponseWriter class', () => { getFormatterForField: () => ({ toJSON: () => '' }), } as any; - return new TabbedAggResponseWriter(new AggConfigs(indexPattern, aggs, { typesRegistry }), { - metricsAtAllLevels: false, - partialRows: false, - ...opts, - }); + return new TabbedAggResponseWriter( + new AggConfigs(indexPattern, aggs, { typesRegistry }, jest.fn()), + { + metricsAtAllLevels: false, + partialRows: false, + ...opts, + } + ); }; describe('Constructor', () => { diff --git a/src/plugins/data/common/search/tabify/tabify.test.ts b/src/plugins/data/common/search/tabify/tabify.test.ts index ad0ed41a05328..d7d983fabfdaf 100644 --- a/src/plugins/data/common/search/tabify/tabify.test.ts +++ b/src/plugins/data/common/search/tabify/tabify.test.ts @@ -32,7 +32,7 @@ describe('tabifyAggResponse Integration', () => { }), } as unknown as DataView; - return new AggConfigs(indexPattern, aggs, { typesRegistry }); + return new AggConfigs(indexPattern, aggs, { typesRegistry }, jest.fn()); }; const mockAggConfig = (agg: any): IAggConfig => agg as unknown as IAggConfig; diff --git a/src/plugins/data/common/search/utils.ts b/src/plugins/data/common/search/utils.ts index ea5ac28852d6a..88b3868c147f7 100644 --- a/src/plugins/data/common/search/utils.ts +++ b/src/plugins/data/common/search/utils.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import moment from 'moment-timezone'; +import { AggTypesDependencies } from '..'; import type { IKibanaSearchResponse } from './types'; /** @@ -28,3 +30,25 @@ export const isCompleteResponse = (response?: IKibanaSearchResponse) => { export const isPartialResponse = (response?: IKibanaSearchResponse) => { return Boolean(response && response.isRunning && response.isPartial); }; + +export const getUserTimeZone = ( + getConfig: AggTypesDependencies['getConfig'], + shouldDetectTimezone: boolean = true +) => { + const defaultTimeZone = 'UTC'; + const userTimeZone = getConfig('dateFormat:tz'); + + if (userTimeZone === 'Browser') { + if (!shouldDetectTimezone) { + return defaultTimeZone; + } + + // If the typeMeta data index template does not have a timezone assigned to the selected field, use the configured tz + const detectedTimezone = moment.tz.guess(); + const tzOffset = moment().format('Z'); + + return detectedTimezone || tzOffset; + } + + return userTimeZone ?? defaultTimeZone; +}; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts index a217e580ad608..bee22e07ab144 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts @@ -10,7 +10,7 @@ import moment from 'moment'; import { createFiltersFromRangeSelectAction } from './create_filters_from_range_select'; -import { IndexPatternsContract } from '../..'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { dataPluginMock } from '../../mocks'; import { setIndexPatterns, setSearchService } from '../../services'; import { FieldFormatsGetConfigFn } from '@kbn/field-formats-plugin/common'; @@ -58,7 +58,7 @@ describe('brushEvent', () => { setIndexPatterns({ ...dataStart.indexPatterns, get: async () => indexPattern, - } as unknown as IndexPatternsContract); + } as unknown as DataViewsContract); baseEvent = { column: 0, diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts index 0b07194cd9bbc..bcfdf46772dc5 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts @@ -9,7 +9,8 @@ import { last } from 'lodash'; import moment from 'moment'; import { Datatable } from '@kbn/expressions-plugin'; -import { esFilters, IFieldType, RangeFilterParams } from '../..'; +import { DataViewFieldBase } from '@kbn/es-query'; +import { esFilters, RangeFilterParams } from '../..'; import { getIndexPatterns, getSearchService } from '../../services'; import { AggConfigSerialized } from '../../../common/search/aggs'; @@ -33,7 +34,7 @@ export async function createFiltersFromRangeSelectAction(event: RangeSelectDataC aggConfigs as AggConfigSerialized, ]); const aggConfig = aggConfigsInstance.aggs[0]; - const field: IFieldType = aggConfig.params.field; + const field: DataViewFieldBase = aggConfig.params.field; if (!field || event.range.length <= 1) { return []; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index 1132edabb2b3f..4bb80fe64134b 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternsContract } from '../..'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { dataPluginMock } from '../../mocks'; import { setIndexPatterns, setSearchService } from '../../services'; import { createFiltersFromValueClickAction } from './create_filters_from_value_click'; @@ -68,7 +68,7 @@ describe('createFiltersFromValueClick', () => { }, getFormatterForField: () => new BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), }), - } as unknown as IndexPatternsContract); + } as unknown as DataViewsContract); }); test('ignores event when value for rows is not provided', async () => { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 52826ed3243d4..978ff66360335 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -58,8 +58,6 @@ import { validateDataView, } from './data_views'; -export type { IndexPatternsService } from './data_views'; - // Index patterns namespace: export const indexPatterns = { ILLEGAL_CHARACTERS_KEY, @@ -74,11 +72,9 @@ export const indexPatterns = { validate: validateDataView, }; -export type { IndexPatternsContract, DataViewsContract, TypeMeta } from './data_views'; +export type { DataViewsContract, TypeMeta } from './data_views'; export type { - IFieldType, - IndexPatternAttributes, AggregationRestrictions as IndexPatternAggRestrictions, IndexPatternLoadExpressionFunctionDefinition, GetFieldsOptions, diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts index e959507823ee5..e0d29a560a436 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts @@ -17,10 +17,10 @@ import { buildFilter, FilterStateStore, FILTERS, + DataViewFieldBase, } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { IFieldType } from '../../../../common'; import { FilterManager } from '../filter_manager'; function getExistingFilter( @@ -69,7 +69,7 @@ function updateExistingFilter(existingFilter: Filter, negate: boolean) { */ export function generateFilters( filterManager: FilterManager, - field: IFieldType | string, + field: DataViewFieldBase | string, values: any, operation: string, index: string @@ -81,7 +81,7 @@ export function generateFilters( : { name: field, } - ) as IFieldType; + ) as DataViewFieldBase; const fieldName = fieldObj.name; const newFilters: Filter[] = []; const appFilters = filterManager.getAppFilters(); @@ -114,7 +114,8 @@ export function generateFilters( const tmpIndexPattern = { id: index } as DataView; // exists filter special case: fieldname = '_exists' and value = fieldname const filterType = fieldName === '_exists_' ? FILTERS.EXISTS : FILTERS.PHRASE; - const actualFieldObj = fieldName === '_exists_' ? ({ name: value } as IFieldType) : fieldObj; + const actualFieldObj = + fieldName === '_exists_' ? ({ name: value } as DataViewFieldBase) : fieldObj; // Fix for #7189 - if value is empty, phrase filters become exists filters. const isNullFilter = value === null || value === undefined; diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_exists.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_exists.test.ts index ea1429743140b..6cac5352af016 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_exists.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_exists.test.ts @@ -8,7 +8,8 @@ import { mapExists } from './map_exists'; import { mapQueryString } from './map_query_string'; -import { IFieldType, buildExistsFilter, buildEmptyFilter } from '../../../../../common'; +import { buildExistsFilter, buildEmptyFilter } from '../../../../../common'; +import { DataViewFieldBase } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/common'; describe('filter manager utilities', () => { @@ -22,7 +23,7 @@ describe('filter manager utilities', () => { }); test('should return the key and value for matching filters', async () => { - const filter = buildExistsFilter({ name: '_type' } as IFieldType, indexPattern); + const filter = buildExistsFilter({ name: '_type' } as DataViewFieldBase, indexPattern); const result = mapExists(filter); expect(result).toHaveProperty('key', '_type'); diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 6dc6e3f9c5874..5a84c03ada672 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -8,20 +8,20 @@ import { Subscription } from 'rxjs'; -import { IUiSettingsClient } from '@kbn/core/public'; -import { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { IUiSettingsClient } from '@kbn/core/public'; +import type { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { DataViewsContract } from '@kbn/data-views-plugin/common'; import { calculateBounds, TimeRange } from '../../../common'; import { + AggConfigs, aggsRequiredUiSettings, AggsCommonStartDependencies, AggsCommonService, - AggConfigs, AggTypesDependencies, } from '../../../common/search/aggs'; -import { AggsSetup, AggsStart } from './types'; -import { IndexPatternsContract } from '../..'; -import { NowProviderInternalContract } from '../../now_provider'; +import type { AggsSetup, AggsStart } from './types'; +import type { NowProviderInternalContract } from '../../now_provider'; /** * Aggs needs synchronous access to specific uiSettings. Since settings can change @@ -59,7 +59,7 @@ export interface AggsSetupDependencies { export interface AggsStartDependencies { fieldFormats: FieldFormatsStart; uiSettings: IUiSettingsClient; - indexPatterns: IndexPatternsContract; + indexPatterns: DataViewsContract; } /** @@ -88,13 +88,15 @@ export class AggsService { return this.aggsCommonService.setup({ registerFunction }); } - public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { - const isDefaultTimezone = () => uiSettings.isDefault('dateFormat:tz'); + public start({ fieldFormats, indexPatterns }: AggsStartDependencies): AggsStart { + const aggExecutionContext: AggTypesDependencies['aggExecutionContext'] = { + shouldDetectTimeZone: true, + }; const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ getConfig: this.getConfig!, getIndexPattern: indexPatterns.get, - isDefaultTimezone, + aggExecutionContext, }); const aggTypesDependencies: AggTypesDependencies = { @@ -104,7 +106,7 @@ export class AggsService { deserialize: fieldFormats.deserialize, getDefaultInstance: fieldFormats.getDefaultInstance, }), - isDefaultTimezone, + aggExecutionContext, }; // initialize each agg type and store in memory @@ -136,7 +138,12 @@ export class AggsService { return { calculateAutoTimeExpression, createAggConfigs: (indexPattern, configStates = []) => { - return new AggConfigs(indexPattern, configStates, { typesRegistry }); + return new AggConfigs( + indexPattern, + configStates, + { typesRegistry, aggExecutionContext }, + this.getConfig! + ); }, types: typesRegistry, }; diff --git a/src/plugins/data/public/search/aggs/mocks.ts b/src/plugins/data/public/search/aggs/mocks.ts index c45d024384ba6..14444a4c0c2f5 100644 --- a/src/plugins/data/public/search/aggs/mocks.ts +++ b/src/plugins/data/public/search/aggs/mocks.ts @@ -57,9 +57,14 @@ export const searchAggsSetupMock = (): AggsSetup => ({ export const searchAggsStartMock = (): AggsStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { - return new AggConfigs(indexPattern, configStates, { - typesRegistry: mockAggTypesRegistry(), - }); + return new AggConfigs( + indexPattern, + configStates, + { + typesRegistry: mockAggTypesRegistry(), + }, + jest.fn() + ); }), types: mockAggTypesRegistry(), }); diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts index 031275e76f3bd..0f1b0bb401461 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { CoreSetup, CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import { usageCollectionPluginMock, Setup } from '@kbn/usage-collection-plugin/public/mocks'; diff --git a/src/plugins/data/public/search/es_search/get_es_preference.test.ts b/src/plugins/data/public/search/es_search/get_es_preference.test.ts index 881e7b6448be4..9a5d341526c47 100644 --- a/src/plugins/data/public/search/es_search/get_es_preference.test.ts +++ b/src/plugins/data/public/search/es_search/get_es_preference.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { getEsPreference } from './get_es_preference'; import { CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/src/plugins/data/public/search/expressions/eql.test.ts b/src/plugins/data/public/search/expressions/eql.test.ts index 3be05be44b039..675ab2442de62 100644 --- a/src/plugins/data/public/search/expressions/eql.test.ts +++ b/src/plugins/data/public/search/expressions/eql.test.ts @@ -7,7 +7,7 @@ */ import { getEql } from './eql'; -import { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { EqlExpressionFunctionDefinition } from '../../../common/search/expressions'; import { StartServicesAccessor } from '@kbn/core/public'; import { DataPublicPluginStart, DataStartDependencies } from '../../types'; diff --git a/src/plugins/data/public/search/expressions/esaggs.test.ts b/src/plugins/data/public/search/expressions/esaggs.test.ts index a837c43d1cf1e..0c5a24adf6e14 100644 --- a/src/plugins/data/public/search/expressions/esaggs.test.ts +++ b/src/plugins/data/public/search/expressions/esaggs.test.ts @@ -8,9 +8,9 @@ import { omit } from 'lodash'; import { of as mockOf } from 'rxjs'; -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import type { ExecutionContext } from '@kbn/expressions-plugin/public'; -import type { IndexPatternsContract } from '../../../common'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import type { ISearchStartSearchSource, KibanaContext, @@ -69,7 +69,7 @@ describe('esaggs expression function - public', () => { } as unknown as jest.Mocked, indexPatterns: { create: jest.fn().mockResolvedValue({}), - } as unknown as jest.Mocked, + } as unknown as jest.Mocked, searchSource: {} as unknown as jest.Mocked, }; getStartDependencies = jest.fn().mockResolvedValue(startDependencies); @@ -112,11 +112,11 @@ describe('esaggs expression function - public', () => { aggs: { foo: 'bar', hierarchical: true, + partialRows: args.partialRows, }, filters: undefined, indexPattern: {}, inspectorAdapters: mockHandlers.inspectorAdapters, - partialRows: args.partialRows, query: undefined, searchSessionId: 'abc123', searchSourceService: startDependencies.searchSource, diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index ffdd1663a87b1..342abc854138e 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -46,6 +46,7 @@ export function getFunctionDefinition({ args.aggs?.map((agg) => agg.value) ?? [] ); aggConfigs.hierarchical = args.metricsAtAllLevels; + aggConfigs.partialRows = args.partialRows; const { handleEsaggsRequest } = await import('../../../common/search/expressions'); @@ -58,7 +59,6 @@ export function getFunctionDefinition({ filters: get(input, 'filters', undefined), indexPattern, inspectorAdapters, - partialRows: args.partialRows, query: get(input, 'query', undefined) as any, searchSessionId: getSearchSessionId(), searchSourceService: searchSource, diff --git a/src/plugins/data/public/search/expressions/esdsl.test.ts b/src/plugins/data/public/search/expressions/esdsl.test.ts index 7ac0d3d5fb133..2bf2ef1148507 100644 --- a/src/plugins/data/public/search/expressions/esdsl.test.ts +++ b/src/plugins/data/public/search/expressions/esdsl.test.ts @@ -7,7 +7,7 @@ */ import { getEsdsl } from './esdsl'; -import { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { EsdslExpressionFunctionDefinition } from '../../../common/search/expressions'; import { StartServicesAccessor } from '@kbn/core/public'; import { DataPublicPluginStart, DataStartDependencies } from '../../types'; diff --git a/src/plugins/data/public/search/expressions/kibana_context.ts b/src/plugins/data/public/search/expressions/kibana_context.ts index 8fad3c8e06c57..cfbac7ad8a5ca 100644 --- a/src/plugins/data/public/search/expressions/kibana_context.ts +++ b/src/plugins/data/public/search/expressions/kibana_context.ts @@ -7,9 +7,9 @@ */ import { StartServicesAccessor } from '@kbn/core/public'; +import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/public'; import { getKibanaContextFn } from '../../../common/search/expressions'; import { DataPublicPluginStart, DataStartDependencies } from '../../types'; -import { SavedObjectsClientCommon } from '../../../common'; /** * This is some glue code that takes in `core.getStartServices`, extracts the dependencies diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index 9287005db38a0..68f1ff1910c98 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { CoreSetup, CoreStart } from '@kbn/core/public'; import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; import { IEsSearchRequest } from '../../../common/search'; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index d717d275f55cd..c8a7c3c9024b5 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { coreMock } from '@kbn/core/public/mocks'; import { CoreSetup, CoreStart } from '@kbn/core/public'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 4755b2f02c183..6c7ec3af0f0d7 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -23,6 +23,7 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import type { ISearchSetup, ISearchStart } from './types'; import { handleResponse } from './fetch'; @@ -55,7 +56,7 @@ import { eqlRawResponse, } from '../../common/search'; import { AggsService, AggsStartDependencies } from './aggs'; -import { IKibanaSearchResponse, IndexPatternsContract, SearchRequest } from '..'; +import { IKibanaSearchResponse, SearchRequest } from '..'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; import { createUsageCollector, SearchUsageCollector } from './collectors'; import { getEsaggs, getEsdsl, getEssql, getEql } from './expressions'; @@ -85,7 +86,7 @@ export interface SearchServiceSetupDependencies { /** @internal */ export interface SearchServiceStartDependencies { fieldFormats: AggsStartDependencies['fieldFormats']; - indexPatterns: IndexPatternsContract; + indexPatterns: DataViewsContract; screenshotMode: ScreenshotModePluginStart; } @@ -230,7 +231,9 @@ export class SearchService implements Plugin { const loadingCount$ = new BehaviorSubject(0); http.addLoadingCountSource(loadingCount$); + const aggs = this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }); const searchSourceDependencies: SearchSourceDependencies = { + aggs, getConfig: uiSettings.get.bind(uiSettings), search, onResponse: (request: SearchRequest, response: IKibanaSearchResponse) => @@ -260,7 +263,7 @@ export class SearchService implements Plugin { } return { - aggs: this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }), + aggs, search, showError: (e: Error) => { this.searchInterceptor.showError(e); diff --git a/src/plugins/data/public/search/session/sessions_mgmt/components/main.test.tsx b/src/plugins/data/public/search/session/sessions_mgmt/components/main.test.tsx index 183dc80883c57..38ccbb9646dee 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/components/main.test.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/components/main.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { MockedKeys } from '@kbn/utility-types/jest'; +import { MockedKeys } from '@kbn/utility-types-jest'; import { mount, ReactWrapper } from 'enzyme'; import { CoreSetup, CoreStart, DocLinksStart } from '@kbn/core/public'; import moment from 'moment'; diff --git a/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.test.tsx b/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.test.tsx index 1e8dd423a8bf9..1fe4dbe0468ee 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.test.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { act, waitFor } from '@testing-library/react'; import { mount, ReactWrapper } from 'enzyme'; import { CoreSetup, CoreStart } from '@kbn/core/public'; diff --git a/src/plugins/data/public/search/session/sessions_mgmt/lib/api.test.ts b/src/plugins/data/public/search/session/sessions_mgmt/lib/api.test.ts index a1d95a83d1ccd..fdc67886f7c00 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/lib/api.test.ts +++ b/src/plugins/data/public/search/session/sessions_mgmt/lib/api.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { CoreSetup, CoreStart } from '@kbn/core/public'; import moment from 'moment'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/src/plugins/data/public/search/session/sessions_mgmt/lib/date_string.ts b/src/plugins/data/public/search/session/sessions_mgmt/lib/date_string.ts index 89ffcb596b094..876385ea7e9c4 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/lib/date_string.ts +++ b/src/plugins/data/public/search/session/sessions_mgmt/lib/date_string.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import moment from 'moment'; +import moment from 'moment-timezone'; import { DATE_STRING_FORMAT } from '../types'; export const dateString = (inputString: string, tz: string): string => { diff --git a/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.test.tsx b/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.test.tsx index 5833ac516fa4b..39f20fec462a2 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.test.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.test.tsx @@ -7,7 +7,7 @@ */ import { EuiTableFieldDataColumnType } from '@elastic/eui'; -import { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { mount } from 'enzyme'; import { CoreSetup, CoreStart } from '@kbn/core/public'; import moment from 'moment'; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index fa23b8f593688..ecf3a0e453e89 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -9,10 +9,10 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { PackageInfo } from '@kbn/core/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { SearchUsageCollector } from './collectors'; import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search'; -import { IndexPatternsContract } from '../../common'; import { ISessionsClient, ISessionService } from './session'; export type { ISearchStartSearchSource, SearchUsageCollector }; @@ -83,5 +83,5 @@ export interface SearchServiceSetupDependencies { /** @internal */ export interface SearchServiceStartDependencies { fieldFormats: AggsStartDependencies['fieldFormats']; - indexPatterns: IndexPatternsContract; + indexPatterns: DataViewsContract; } diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index cb294964a4b4f..0705c6b77c441 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -8,7 +8,7 @@ import { NotificationsStart, CoreStart, ThemeServiceStart } from '@kbn/core/public'; import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; -import { IndexPatternsContract } from './data_views'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { DataPublicPluginStart } from './types'; export const [getNotifications, setNotifications] = @@ -20,7 +20,7 @@ export const [getUiSettings, setUiSettings] = export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); export const [getIndexPatterns, setIndexPatterns] = - createGetterSetter('IndexPatterns'); + createGetterSetter('IndexPatterns'); export const [getSearchService, setSearchService] = createGetterSetter('Search'); diff --git a/src/plugins/data/server/datatable_utilities/datatable_utilities_service.ts b/src/plugins/data/server/datatable_utilities/datatable_utilities_service.ts index 9c460e639dc37..57d130f30aa98 100644 --- a/src/plugins/data/server/datatable_utilities/datatable_utilities_service.ts +++ b/src/plugins/data/server/datatable_utilities/datatable_utilities_service.ts @@ -12,14 +12,14 @@ import type { UiSettingsServiceStart, } from '@kbn/core/server'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; -import type { IndexPatternsServiceStart } from '@kbn/data-views-plugin/server'; +import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { DatatableUtilitiesService as DatatableUtilitiesServiceCommon } from '../../common'; import type { AggsStart } from '../search'; export class DatatableUtilitiesService { constructor( private aggs: AggsStart, - private dataViews: IndexPatternsServiceStart, + private dataViews: DataViewsServerPluginStart, private fieldFormats: FieldFormatsStart, private uiSettings: UiSettingsServiceStart ) { diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 1c7c67318fccd..21c4fd42831c1 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -28,30 +28,18 @@ export const exporters = { CSV_MIME_TYPE, }; -/* - * Field Formats: - */ - -export { DATA_VIEW_SAVED_OBJECT_TYPE } from '../common'; - /* * Index patterns: */ -export type { FieldDescriptor, IndexPatternsServiceStart } from './data_views'; -export { - IndexPatternsFetcher, - shouldReadFieldFromDocValues, - getCapabilitiesForRollupIndices, -} from './data_views'; +export type { FieldDescriptor, DataViewsServerPluginStart } from './data_views'; +export { IndexPatternsFetcher, getCapabilitiesForRollupIndices } from './data_views'; -export type { IndexPatternAttributes } from '../common'; export { ES_FIELD_TYPES, KBN_FIELD_TYPES, UI_SETTINGS, - IndexPatternsService, - IndexPatternsService as IndexPatternsCommonService, + DataViewsService as DataViewsCommonService, DataView, } from '../common'; diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index 4885c5d61f039..beaabb044511b 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -15,15 +15,15 @@ import { } from '@kbn/core/server'; import { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; +import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { - AggsCommonService, AggConfigs, + AggsCommonService, AggTypesDependencies, aggsRequiredUiSettings, calculateBounds, TimeRange, } from '../../../common'; -import { IndexPatternsServiceStart } from '../../data_views'; import { AggsSetup, AggsStart } from './types'; /** @internal */ @@ -35,7 +35,7 @@ export interface AggsSetupDependencies { export interface AggsStartDependencies { fieldFormats: FieldFormatsStart; uiSettings: UiSettingsServiceStart; - indexPatterns: IndexPatternsServiceStart; + indexPatterns: DataViewsServerPluginStart; } /** @@ -70,29 +70,27 @@ export class AggsService { const getConfig = (key: string): T => { return uiSettingsCache[key]; }; - const isDefaultTimezone = () => getConfig('dateFormat:tz') === 'Browser'; + + const aggExecutionContext: AggTypesDependencies['aggExecutionContext'] = { + shouldDetectTimeZone: false, + }; const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ getConfig, + aggExecutionContext, getIndexPattern: ( - await indexPatterns.indexPatternsServiceFactory(savedObjectsClient, elasticsearchClient) + await indexPatterns.dataViewsServiceFactory(savedObjectsClient, elasticsearchClient) ).get, - isDefaultTimezone, }); const aggTypesDependencies: AggTypesDependencies = { calculateBounds: this.calculateBounds, + aggExecutionContext, getConfig, getFieldFormatsStart: () => ({ deserialize: formats.deserialize, getDefaultInstance: formats.getDefaultInstance, }), - /** - * Date histogram and date range need to know whether we are using the - * default timezone, but `isDefault` is not currently offered on the - * server, so we need to manually check for the default value. - */ - isDefaultTimezone, }; const typesRegistry = { @@ -114,9 +112,13 @@ export class AggsService { return { calculateAutoTimeExpression, - createAggConfigs: (indexPattern, configStates = []) => { - return new AggConfigs(indexPattern, configStates, { typesRegistry }); - }, + createAggConfigs: (indexPattern, configStates = []) => + new AggConfigs( + indexPattern, + configStates, + { typesRegistry, aggExecutionContext }, + getConfig + ), types: typesRegistry, }; }, diff --git a/src/plugins/data/server/search/aggs/mocks.ts b/src/plugins/data/server/search/aggs/mocks.ts index 301bc3e5e1240..093493c9b3bbf 100644 --- a/src/plugins/data/server/search/aggs/mocks.ts +++ b/src/plugins/data/server/search/aggs/mocks.ts @@ -59,9 +59,14 @@ export const searchAggsSetupMock = (): AggsSetup => ({ const commonStartMock = (): AggsCommonStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { - return new AggConfigs(indexPattern, configStates, { - typesRegistry: mockAggTypesRegistry(), - }); + return new AggConfigs( + indexPattern, + configStates, + { + typesRegistry: mockAggTypesRegistry(), + }, + jest.fn() + ); }), types: mockAggTypesRegistry(), }); diff --git a/src/plugins/data/server/search/expressions/esaggs.test.ts b/src/plugins/data/server/search/expressions/esaggs.test.ts index bd07d8f538456..8f7ec47e18f47 100644 --- a/src/plugins/data/server/search/expressions/esaggs.test.ts +++ b/src/plugins/data/server/search/expressions/esaggs.test.ts @@ -8,10 +8,10 @@ import { omit } from 'lodash'; import { of as mockOf } from 'rxjs'; -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { KibanaRequest } from '@kbn/core/server'; import type { ExecutionContext } from '@kbn/expressions-plugin/server'; -import type { IndexPatternsContract } from '../../../common'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import type { AggsCommonStart, ISearchStartSearchSource, @@ -71,7 +71,7 @@ describe('esaggs expression function - server', () => { } as unknown as jest.Mocked, indexPatterns: { create: jest.fn().mockResolvedValue({}), - } as unknown as jest.Mocked, + } as unknown as jest.Mocked, searchSource: {} as unknown as jest.Mocked, }; getStartDependencies = jest.fn().mockResolvedValue(startDependencies); @@ -120,11 +120,11 @@ describe('esaggs expression function - server', () => { aggs: { foo: 'bar', hierarchical: args.metricsAtAllLevels, + partialRows: args.partialRows, }, filters: undefined, indexPattern: {}, inspectorAdapters: mockHandlers.inspectorAdapters, - partialRows: args.partialRows, query: undefined, searchSessionId: 'abc123', searchSourceService: startDependencies.searchSource, diff --git a/src/plugins/data/server/search/expressions/esaggs.ts b/src/plugins/data/server/search/expressions/esaggs.ts index 912acfab3b3fe..bca2ac63b7f0f 100644 --- a/src/plugins/data/server/search/expressions/esaggs.ts +++ b/src/plugins/data/server/search/expressions/esaggs.ts @@ -60,6 +60,7 @@ export function getFunctionDefinition({ ); aggConfigs.hierarchical = args.metricsAtAllLevels; + aggConfigs.partialRows = args.partialRows; return { aggConfigs, indexPattern, searchSource }; }).pipe( @@ -70,7 +71,6 @@ export function getFunctionDefinition({ filters: get(input, 'filters', undefined), indexPattern, inspectorAdapters, - partialRows: args.partialRows, query: get(input, 'query', undefined) as any, searchSessionId: getSearchSessionId(), searchSourceService: searchSource, @@ -111,7 +111,7 @@ export function getEsaggs({ return { aggs: await search.aggs.asScopedToClient(savedObjectsClient, esClient.asCurrentUser), - indexPatterns: await indexPatterns.indexPatternsServiceFactory( + indexPatterns: await indexPatterns.dataViewsServiceFactory( savedObjectsClient, esClient.asCurrentUser ), diff --git a/src/plugins/data/server/search/expressions/kibana_context.ts b/src/plugins/data/server/search/expressions/kibana_context.ts index ee28124d09216..14fdcdd784f33 100644 --- a/src/plugins/data/server/search/expressions/kibana_context.ts +++ b/src/plugins/data/server/search/expressions/kibana_context.ts @@ -7,9 +7,9 @@ */ import { StartServicesAccessor } from '@kbn/core/server'; +import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/server'; import { getKibanaContextFn } from '../../../common/search/expressions'; import { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; -import { SavedObjectsClientCommon } from '../../../common'; /** * This is some glue code that takes in `core.getStartServices`, extracts the dependencies diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts index f31e5e8be6de8..c10401dd12f77 100644 --- a/src/plugins/data/server/search/routes/search.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { from } from 'rxjs'; import { CoreSetup, RequestHandlerContext } from '@kbn/core/server'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; diff --git a/src/plugins/data/server/search/routes/session.test.ts b/src/plugins/data/server/search/routes/session.test.ts index 20dbac04a7674..dbc12e44b7a2b 100644 --- a/src/plugins/data/server/search/routes/session.test.ts +++ b/src/plugins/data/server/search/routes/session.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import type { CoreSetup, Logger } from '@kbn/core/server'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 663b28e733e29..b0748c0aa9347 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { CoreSetup, CoreStart, SavedObject } from '@kbn/core/server'; import { coreMock } from '@kbn/core/server/mocks'; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a26f7b36df56b..6561e6e127d0b 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -30,6 +30,7 @@ import type { TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import type { DataRequestHandlerContext, IScopedSearchClient, @@ -41,7 +42,6 @@ import type { import { AggsService } from './aggs'; -import { IndexPatternsServiceStart } from '../data_views'; import { registerSearchRoute, registerSessionRoutes } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './strategies/es_search'; import { DataPluginStart, DataPluginStartDependencies } from '../plugin'; @@ -116,7 +116,7 @@ export interface SearchServiceSetupDependencies { /** @internal */ export interface SearchServiceStartDependencies { fieldFormats: FieldFormatsStart; - indexPatterns: IndexPatternsServiceStart; + indexPatterns: DataViewsServerPluginStart; taskManager?: TaskManagerStartContract; } @@ -275,13 +275,15 @@ export class SearchService implements Plugin { this.sessionService.start(core, { taskManager }); } + const aggs = this.aggsService.start({ + fieldFormats, + uiSettings, + indexPatterns, + }); + this.asScoped = this.asScopedProvider(core); return { - aggs: this.aggsService.start({ - fieldFormats, - uiSettings, - indexPatterns, - }), + aggs, searchAsInternalUser: this.searchAsInternalUser, getSearchStrategy: this.getSearchStrategy, asScoped: this.asScoped, @@ -289,11 +291,12 @@ export class SearchService implements Plugin { asScoped: async (request: KibanaRequest) => { const esClient = elasticsearch.client.asScoped(request); const savedObjectsClient = savedObjects.getScopedClient(request); - const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory( + const scopedIndexPatterns = await indexPatterns.dataViewsServiceFactory( savedObjectsClient, esClient.asCurrentUser ); const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + const aggsStart = await aggs.asScopedToClient(savedObjectsClient, esClient.asCurrentUser); // cache ui settings, only including items which are explicitly needed by SearchSource const uiSettingsCache = pick( @@ -302,6 +305,7 @@ export class SearchService implements Plugin { ); const searchSourceDependencies: SearchSourceDependencies = { + aggs: aggsStart, getConfig: (key: string): T => uiSettingsCache[key], search: this.asScoped(request).search, onResponse: (req, res) => res, diff --git a/src/plugins/data/server/search/search_source/mocks.ts b/src/plugins/data/server/search/search_source/mocks.ts index 6c6e2cf8a8736..c22cf033ca6a6 100644 --- a/src/plugins/data/server/search/search_source/mocks.ts +++ b/src/plugins/data/server/search/search_source/mocks.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { KibanaRequest } from '@kbn/core/server'; import { searchSourceCommonMock } from '../../../common/search/search_source/mocks'; diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/date/date.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/date/date.tsx index 73ab4b58bbcb0..fdf0291f0fa4d 100644 --- a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/date/date.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/date/date.tsx @@ -35,7 +35,7 @@ export class DateFormatEditor extends DefaultFormatEditor diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx index 8a82caa1e7f03..468263fcdfc38 100644 --- a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx @@ -34,7 +34,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/duration/duration.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/duration/duration.tsx index e18c9664cacab..8b8864622ffc2 100644 --- a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/duration/duration.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/duration/duration.tsx @@ -86,7 +86,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< const { error, samples, hasDecimalError } = this.state; const formatParams: DurationFormatEditorFormatParams = { - includeSpaceWithSuffix: format.getParamDefaults().includeSpaceWithSuffix, + includeSpaceWithSuffix: format.getParamDefaults().includeSpaceWithSuffix as boolean, ...this.props.formatParams, }; diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/number/number.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/number/number.tsx index 65793583d8bf3..5d9f2e0c87595 100644 --- a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/number/number.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/number/number.tsx @@ -34,7 +34,7 @@ export class NumberFormatEditor extends DefaultFormatEditor diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/url/url.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/url/url.tsx index d7f76e07ef447..ed458f7a040ff 100644 --- a/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/url/url.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/editors/url/url.tsx @@ -148,7 +148,7 @@ export class UrlFormatEditor extends DefaultFormatEditor< const { formatParams, format } = this.props; const { error, samples, sampleConverterType } = this.state; - const urlType = formatParams.type ?? format.getParamDefaults().type; + const urlType = formatParams.type ?? `${format.getParamDefaults().type}`; return ( { name: 'scriptedFieldWithEmptyFormatter', type: 'number', esTypes: ['long'], + searchable: true, + aggregatable: true, }) ).toEqual( expect.objectContaining({ diff --git a/src/plugins/data_views/common/data_views/data_view.ts b/src/plugins/data_views/common/data_views/data_view.ts index 2cb587a79a9e2..7d52ec86c5227 100644 --- a/src/plugins/data_views/common/data_views/data_view.ts +++ b/src/plugins/data_views/common/data_views/data_view.ts @@ -18,8 +18,6 @@ import { import type { DataViewBase } from '@kbn/es-query'; import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '..'; import type { RuntimeField, RuntimeFieldSpec, RuntimeType, FieldConfiguration } from '../types'; - -import { IFieldType } from '..'; import { DataViewField, IIndexPatternFieldList, fieldList } from '../fields'; import { flattenHitWrapper } from './flatten_hit'; import { DataViewSpec, TypeMeta, SourceFilter, DataViewFieldMap } from '../types'; @@ -47,47 +45,102 @@ interface SavedObjectBody { * An interface representing a data view that is time based. */ export interface TimeBasedDataView extends DataView { + /** + * The timestamp field name. + */ timeFieldName: NonNullable; + /** + * The timestamp field. + */ getTimeField: () => DataViewField; } +/** + * Data view class. Central kibana abstraction around multiple indices. + */ export class DataView implements DataViewBase { + /** + * Saved object id + */ public id?: string; + /** + * Title of data view + */ public title: string = ''; + /** + * Map of field formats by field name + */ public fieldFormatMap: Record; /** - * Only used by rollup indices, used by rollup specific endpoint to load field list + * Only used by rollup indices, used by rollup specific endpoint to load field list. */ public typeMeta?: TypeMeta; + /** + * Field list, in extended array format + */ public fields: IIndexPatternFieldList & { toSpec: () => DataViewFieldMap }; + /** + * Timestamp field name + */ public timeFieldName: string | undefined; /** - * Type is used to identify rollup index patterns + * Type is used to identify rollup index patterns. */ public type: string | undefined; /** * @deprecated Use `flattenHit` utility method exported from data plugin instead. */ public flattenHit: (hit: Record, deep?: boolean) => Record; + /** + * List of meta fields by name + */ public metaFields: string[]; /** * SavedObject version */ public version: string | undefined; + /** + * Array of filters - hides fields in discover + */ public sourceFilters?: SourceFilter[]; + /** + * Array of namespace ids + */ public namespaces: string[]; + /** + * Original saved object body. Used to check for saved object changes. + */ private originalSavedObjectBody: SavedObjectBody = {}; + /** + * Returns true if short dot notation is enabled + */ private shortDotsEnable: boolean = false; + /** + * FieldFormats service interface + */ private fieldFormats: FieldFormatsStartCommon; + /** + * Map of field attributes by field name. Currently count and customLabel. + */ private fieldAttrs: FieldAttrs; + /** + * Map of runtime field definitions by field name + */ private runtimeFieldMap: Record; /** - * prevents errors when index pattern exists before indices + * Prevents errors when index pattern exists before indices */ public readonly allowNoIndex: boolean = false; - constructor({ spec = {}, fieldFormats, shortDotsEnable = false, metaFields = [] }: DataViewDeps) { + /** + * constructor + * @param config - config data and dependencies + */ + + constructor(config: DataViewDeps) { + const { spec = {}, fieldFormats, shortDotsEnable = false, metaFields = [] } = config; + // set dependencies this.fieldFormats = fieldFormats; // set config @@ -122,12 +175,15 @@ export class DataView implements DataViewBase { getOriginalSavedObjectBody = () => ({ ...this.originalSavedObjectBody }); /** - * Reset last saved saved object fields. used after saving + * Reset last saved saved object fields. Used after saving. */ resetOriginalSavedObjectBody = () => { this.originalSavedObjectBody = this.getAsSavedObjectBody(); }; + /** + * Returns field attributes map + */ getFieldAttrs = () => { const newFieldAttrs = { ...this.fieldAttrs }; @@ -153,6 +209,10 @@ export class DataView implements DataViewBase { return newFieldAttrs; }; + /** + * Returns scripted fields + */ + getComputedFields() { const scriptFields: Record = {}; if (!this.fields) { @@ -197,7 +257,7 @@ export class DataView implements DataViewBase { } /** - * Create static representation of index pattern + * Creates static representation of the data view. */ public toSpec(): DataViewSpec { return { @@ -227,8 +287,8 @@ export class DataView implements DataViewBase { } /** - * Remove scripted field from field list - * @param fieldName + * Removes scripted field from field list. + * @param fieldName name of scripted field to remove * @deprecated use runtime field instead */ @@ -241,7 +301,7 @@ export class DataView implements DataViewBase { /** * - * @deprecated Will be removed when scripted fields are removed + * @deprecated Will be removed when scripted fields are removed. */ getNonScriptedFields() { return [...this.fields.getAll().filter((field) => !field.scripted)]; @@ -249,31 +309,51 @@ export class DataView implements DataViewBase { /** * - * @deprecated use runtime field instead + * @deprecated Use runtime field instead. */ getScriptedFields() { return [...this.fields.getAll().filter((field) => field.scripted)]; } + /** + * Does the data view have a timestamp field? + */ + isTimeBased(): this is TimeBasedDataView { return !!this.timeFieldName && (!this.fields || !!this.getTimeField()); } + /** + * Does the data view have a timestamp field and is it a date nanos field? + */ + isTimeNanosBased(): this is TimeBasedDataView { const timeField = this.getTimeField(); return !!(timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1); } + /** + * Get timestamp field as DataViewField or return undefined + */ getTimeField() { if (!this.timeFieldName || !this.fields || !this.fields.getByName) return undefined; return this.fields.getByName(this.timeFieldName); } + /** + * Get field by name. + * @param name field name + */ + getFieldByName(name: string): DataViewField | undefined { if (!this.fields || !this.fields.getByName) return undefined; return this.fields.getByName(name); } + /** + * Get aggregation restrictions. Rollup fields can only perform a subset of aggregations. + */ + getAggregationRestrictions() { return this.typeMeta?.aggs; } @@ -304,9 +384,9 @@ export class DataView implements DataViewBase { /** * Provide a field, get its formatter - * @param field + * @param field field to get formatter for */ - getFormatterForField(field: DataViewField | DataViewField['spec'] | IFieldType): FieldFormat { + getFormatterForField(field: DataViewField | DataViewField['spec']): FieldFormat { const fieldFormat = this.getFormatterForFieldNoDefault(field.name); if (fieldFormat) { return fieldFormat; @@ -352,7 +432,7 @@ export class DataView implements DataViewBase { /** * Checks if runtime field exists - * @param name + * @param name field name */ hasRuntimeField(name: string): boolean { return !!this.runtimeFieldMap[name]; @@ -360,7 +440,7 @@ export class DataView implements DataViewBase { /** * Returns runtime field if exists - * @param name + * @param name Runtime field name */ getRuntimeField(name: string): RuntimeField | null { if (!this.runtimeFieldMap[name]) { @@ -380,6 +460,11 @@ export class DataView implements DataViewBase { return runtimeField; } + /** + * Get all runtime field definitions. + * @returns map of runtime field definitions by field name + */ + getAllRuntimeFields(): Record { return Object.keys(this.runtimeFieldMap).reduce>( (acc, fieldName) => ({ @@ -390,6 +475,12 @@ export class DataView implements DataViewBase { ); } + /** + * Returns data view fields backed by runtime fields. + * @param name runtime field name + * @returns map of DataViewFields (that are runtime fields) by field name + */ + getFieldsByRuntimeFieldName(name: string): Record | undefined { const runtimeField = this.getRuntimeField(name); if (!runtimeField) { @@ -420,8 +511,8 @@ export class DataView implements DataViewBase { } /** - * Replaces all existing runtime fields with new fields - * @param newFields + * Replaces all existing runtime fields with new fields. + * @param newFields Map of runtime field definitions by field name */ replaceAllRuntimeFields(newFields: Record) { const oldRuntimeFieldNames = Object.keys(this.runtimeFieldMap); @@ -454,7 +545,7 @@ export class DataView implements DataViewBase { } /** - * Return the "runtime_mappings" section of the ES search query + * Return the "runtime_mappings" section of the ES search query. */ getRuntimeMappings(): estypes.MappingRuntimeFields { // @ts-expect-error The ES client does not yet include the "composite" runtime type @@ -462,8 +553,8 @@ export class DataView implements DataViewBase { } /** - * Get formatter for a given field name. Return undefined if none exists - * @param field + * Get formatter for a given field name. Return undefined if none exists. + * @param fieldname name of field to get formatter for */ getFormatterForFieldNoDefault(fieldname: string) { const formatSpec = this.fieldFormatMap[fieldname]; @@ -472,6 +563,13 @@ export class DataView implements DataViewBase { } } + /** + * Set field attribute + * @param fieldName name of field to set attribute on + * @param attrName name of attribute to set + * @param value value of attribute + */ + protected setFieldAttrs( fieldName: string, attrName: K, @@ -483,6 +581,12 @@ export class DataView implements DataViewBase { this.fieldAttrs[fieldName][attrName] = value; } + /** + * Set field custom label + * @param fieldName name of field to set custom label on + * @param customLabel custom label value. If undefined, custom label is removed + */ + public setFieldCustomLabel(fieldName: string, customLabel: string | undefined | null) { const fieldObject = this.fields.getByName(fieldName); const newCustomLabel: string | undefined = customLabel === null ? undefined : customLabel; @@ -494,6 +598,12 @@ export class DataView implements DataViewBase { this.setFieldAttrs(fieldName, 'customLabel', newCustomLabel); } + /** + * Set field count + * @param fieldName name of field to set count on + * @param count count value. If undefined, count is removed + */ + public setFieldCount(fieldName: string, count: number | undefined | null) { const fieldObject = this.fields.getByName(fieldName); const newCount: number | undefined = count === null ? undefined : count; @@ -505,14 +615,31 @@ export class DataView implements DataViewBase { this.setFieldAttrs(fieldName, 'count', newCount); } + /** + * Set field formatter + * @param fieldName name of field to set format on + * @param format field format in serialized form + */ public readonly setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { this.fieldFormatMap[fieldName] = format; }; + /** + * Remove field format from the field format map. + * @param fieldName field name associated with the format for removal + */ + public readonly deleteFieldFormat = (fieldName: string) => { delete this.fieldFormatMap[fieldName]; }; + /** + * Add composite runtime field and all subfields. + * @param name field name + * @param runtimeField runtime field definition + * @returns data view field instance + */ + private addCompositeRuntimeField(name: string, runtimeField: RuntimeField): DataViewField[] { const { fields } = runtimeField; 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 b263c7d77fe3b..fc7b3665d8e6b 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -6,23 +6,17 @@ * Side Public License, v 1. */ -/* eslint-disable max-classes-per-file */ - import { i18n } from '@kbn/i18n'; import { PublicMethodsOf } from '@kbn/utility-types'; import { castEsToKbnFieldTypeName } from '@kbn/field-types'; import { FieldFormatsStartCommon, FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common'; import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; -import { - DATA_VIEW_SAVED_OBJECT_TYPE, - DEFAULT_ASSETS_TO_IGNORE, - SavedObjectsClientCommon, -} from '..'; +import { DATA_VIEW_SAVED_OBJECT_TYPE, DEFAULT_ASSETS_TO_IGNORE } from '..'; +import { SavedObjectsClientCommon } from '../types'; import { createDataViewCache } from '.'; import type { RuntimeField, RuntimeFieldSpec, RuntimeType } from '../types'; import { DataView } from './data_view'; -import { createEnsureDefaultDataView, EnsureDefaultDataView } from './ensure_default_data_view'; import { OnNotification, OnError, @@ -43,63 +37,209 @@ import { DuplicateDataViewError, DataViewInsufficientAccessError } from '../erro const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; -export type IndexPatternSavedObjectAttrs = Pick; +export type DataViewSavedObjectAttrs = Pick; export type IndexPatternListSavedObjectAttrs = Pick< DataViewAttributes, 'title' | 'type' | 'typeMeta' >; +/** + * Result from data view search - summary data. + */ export interface DataViewListItem { + /** + * Saved object id + */ id: string; + /** + * Namespace ids + */ namespaces?: string[]; + /** + * Data view title + */ title: string; + /** + * Data view type + */ type?: string; + /** + * Data view type meta + */ typeMeta?: TypeMeta; } +/** + * Data views API service dependencies + */ export interface DataViewsServiceDeps { + /** + * UiSettings service instance wrapped in a common interface + */ uiSettings: UiSettingsCommon; + /** + * Saved objects client interface wrapped in a common interface + */ savedObjectsClient: SavedObjectsClientCommon; + /** + * Wrapper around http call functionality so it can be used on client or server + */ apiClient: IDataViewsApiClient; + /** + * Field formats service + */ fieldFormats: FieldFormatsStartCommon; + /** + * Hander for service notifications + */ onNotification: OnNotification; + /** + * Handler for service errors + */ onError: OnError; + /** + * Redirects when there's no data view. only used on client + */ onRedirectNoIndexPattern?: () => void; + /** + * Determines whether the user can save data views + */ getCanSave: () => Promise; } +/** + * Data views API service methods + * @public + */ export interface DataViewsServicePublicMethods { + /** + * Clear the cache of data views. + * @param id + */ clearCache: (id?: string | undefined) => void; + /** + * Create data view based on the provided spec. + * @param spec - Data view spec. + * @param skipFetchFields - If true, do not fetch fields. + */ create: (spec: DataViewSpec, skipFetchFields?: boolean) => Promise; + /** + * Create and save data view based on provided spec. + * @param spec - Data view spec. + * @param override - If true, save over existing data view + * @param skipFetchFields - If true, do not fetch fields. + */ createAndSave: ( spec: DataViewSpec, override?: boolean, skipFetchFields?: boolean ) => Promise; + /** + * Save data view + * @param dataView - Data view instance to save. + * @param override - If true, save over existing data view + */ createSavedObject: (indexPattern: DataView, override?: boolean) => Promise; + /** + * Delete data view + * @param indexPatternId - Id of the data view to delete. + */ delete: (indexPatternId: string) => Promise<{}>; - ensureDefaultDataView: EnsureDefaultDataView; + /** + * Takes field array and field attributes and returns field map by name. + * @param fields - Array of fieldspecs + * @params fieldAttrs - Field attributes, map by name + * @returns Field map by name + */ fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => DataViewFieldMap; + /** + * Search for data views based on title + * @param search - Search string + * @param size - Number of results to return + */ find: (search: string, size?: number) => Promise; + /** + * Get data view by id. + * @param id - Id of the data view to get. + */ get: (id: string) => Promise; - getCache: () => Promise> | null | undefined>; + /** + * Get populated data view saved object cache. + */ + getCache: () => Promise> | null | undefined>; + /** + * If user can save data view, return true. + */ getCanSave: () => Promise; + /** + * Get default data view as data view instance. + */ getDefault: () => Promise; + /** + * Get default data view id. + */ getDefaultId: () => Promise; + /** + * Get default data view, if it doesn't exist, choose and save new default data view and return it. + */ getDefaultDataView: () => Promise; + /** + * Get fields for data view + * @param dataView - Data view instance or spec + * @param options - Options for getting fields + * @returns FieldSpec array + */ getFieldsForIndexPattern: ( indexPattern: DataView | DataViewSpec, options?: GetFieldsOptions | undefined ) => Promise; + /** + * Get fields for index pattern string + * @param options - options for getting fields + */ getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + /** + * Get list of data view ids. + * @param refresh - clear cache and fetch from server + */ getIds: (refresh?: boolean) => Promise; + /** + * Get list of data view ids and title (and more) for each data view. + * @param refresh - clear cache and fetch from server + */ getIdsWithTitle: (refresh?: boolean) => Promise; + /** + * Get list of data view ids and title (and more) for each data view. + * @param refresh - clear cache and fetch from server + */ getTitles: (refresh?: boolean) => Promise; + /** + * Returns true if user has access to view a data view. + */ hasUserDataView: () => Promise; + /** + * Refresh fields for data view instance + * @params dataView - Data view instance + */ refreshFields: (indexPattern: DataView) => Promise; + /** + * Converts data view saved object to spec + * @params savedObject - Data view saved object + */ savedObjectToSpec: (savedObject: SavedObject) => DataViewSpec; + /** + * Set default data view. + * @param id - Id of the data view to set as default. + * @param force - Overwrite if true + */ setDefault: (id: string | null, force?: boolean) => Promise; + /** + * Save saved object + * @param indexPattern - data view instance + * @param saveAttempts - number of times to try saving + * @oaram ignoreErrors - if true, do not throw error on failure + */ updateSavedObject: ( indexPattern: DataView, saveAttempts?: number, @@ -107,60 +247,64 @@ export interface DataViewsServicePublicMethods { ) => Promise; } +/** + * Data views service, providing CRUD operations for data views. + * @public + */ export class DataViewsService { private config: UiSettingsCommon; private savedObjectsClient: SavedObjectsClientCommon; - private savedObjectsCache?: Array> | null; + private savedObjectsCache?: Array> | null; private apiClient: IDataViewsApiClient; private fieldFormats: FieldFormatsStartCommon; /** - * Handler for service notifications + * Handler for service notifications * @param toastInputFields notification content in toast format * @param key used to indicate uniqueness of the notification */ private onNotification: OnNotification; /* - * Handler for service errors + * Handler for service errors * @param error notification content in toast format * @param key used to indicate uniqueness of the error */ private onError: OnError; private dataViewCache: ReturnType; + /** + * Can the user save data views? + */ public getCanSave: () => Promise; /** - * @deprecated Use `getDefaultDataView` instead (when loading data view) and handle - * 'no data view' case in api consumer code - no more auto redirect + * DataViewsService constructor + * @param deps Service dependencies */ - ensureDefaultDataView: EnsureDefaultDataView; - - constructor({ - uiSettings, - savedObjectsClient, - apiClient, - fieldFormats, - onNotification, - onError, - onRedirectNoIndexPattern = () => {}, - getCanSave = () => Promise.resolve(false), - }: DataViewsServiceDeps) { + constructor(deps: DataViewsServiceDeps) { + const { + uiSettings, + savedObjectsClient, + apiClient, + fieldFormats, + onNotification, + onError, + getCanSave = () => Promise.resolve(false), + } = deps; this.apiClient = apiClient; this.config = uiSettings; this.savedObjectsClient = savedObjectsClient; this.fieldFormats = fieldFormats; this.onNotification = onNotification; this.onError = onError; - this.ensureDefaultDataView = createEnsureDefaultDataView(uiSettings, onRedirectNoIndexPattern); this.getCanSave = getCanSave; this.dataViewCache = createDataViewCache(); } /** - * Refresh cache of index pattern ids and titles + * Refresh cache of index pattern ids and titles. */ private async refreshSavedObjectsCache() { - const so = await this.savedObjectsClient.find({ + const so = await this.savedObjectsClient.find({ type: DATA_VIEW_SAVED_OBJECT_TYPE, fields: ['title', 'type', 'typeMeta'], perPage: 10000, @@ -169,7 +313,7 @@ export class DataViewsService { } /** - * Get list of index pattern ids + * Gets list of index pattern ids. * @param refresh Force refresh of index pattern list */ getIds = async (refresh: boolean = false) => { @@ -183,7 +327,7 @@ export class DataViewsService { }; /** - * Get list of index pattern titles + * Gets list of index pattern titles. * @param refresh Force refresh of index pattern list */ getTitles = async (refresh: boolean = false): Promise => { @@ -197,13 +341,13 @@ export class DataViewsService { }; /** - * Find and load index patterns by title - * @param search - * @param size - * @returns IndexPattern[] + * Find and load index patterns by title. + * @param search Search string + * @param size Number of data views to return + * @returns DataView[] */ find = async (search: string, size: number = 10): Promise => { - const savedObjects = await this.savedObjectsClient.find({ + const savedObjects = await this.savedObjectsClient.find({ type: DATA_VIEW_SAVED_OBJECT_TYPE, fields: ['title'], search, @@ -217,7 +361,7 @@ export class DataViewsService { }; /** - * Get list of index pattern ids with titles + * Gets list of index pattern ids with titles. * @param refresh Force refresh of index pattern list */ getIdsWithTitle = async (refresh: boolean = false): Promise => { @@ -237,7 +381,7 @@ export class DataViewsService { }; /** - * Clear index pattern list cache + * Clear index pattern list cache. * @param id optionally clear a single id */ clearCache = (id?: string) => { @@ -249,6 +393,10 @@ export class DataViewsService { } }; + /** + * Get cache, contains data view saved objects. + */ + getCache = async () => { if (!this.savedObjectsCache) { await this.refreshSavedObjectsCache(); @@ -278,8 +426,8 @@ export class DataViewsService { /** * Optionally set default index pattern, unless force = true - * @param id - * @param force + * @param id data view id + * @param force set default data view even if there's an existing default */ setDefault = async (id: string | null, force = false) => { if (force || !(await this.config.get('defaultIndex'))) { @@ -288,15 +436,15 @@ export class DataViewsService { }; /** - * Checks if current user has a user created index pattern ignoring fleet's server default index patterns + * Checks if current user has a user created index pattern ignoring fleet's server default index patterns. */ async hasUserDataView(): Promise { return this.apiClient.hasUserIndexPattern(); } /** - * Get field list by providing { pattern } - * @param options + * Get field list by providing { pattern }. + * @param options options for getting field list * @returns FieldSpec[] */ getFieldsForWildcard = async (options: GetFieldsOptions): Promise => { @@ -312,8 +460,8 @@ export class DataViewsService { }; /** - * Get field list by providing an index patttern (or spec) - * @param options + * Get field list by providing an index patttern (or spec). + * @param options options for getting field list * @returns FieldSpec[] */ getFieldsForIndexPattern = async ( @@ -328,7 +476,7 @@ export class DataViewsService { }); /** - * Refresh field list for a given index pattern + * Refresh field list for a given index pattern. * @param indexPattern */ refreshFields = async (indexPattern: DataView) => { @@ -363,7 +511,7 @@ export class DataViewsService { }; /** - * Refreshes a field list from a spec before an index pattern instance is created + * Refreshes a field list from a spec before an index pattern instance is created. * @param fields * @param id * @param title @@ -417,7 +565,7 @@ export class DataViewsService { }; /** - * Converts field array to map + * Converts field array to map. * @param fields: FieldSpec[] * @param fieldAttrs: FieldAttrs * @returns Record @@ -433,7 +581,7 @@ export class DataViewsService { }, {}); /** - * Converts index pattern saved object to index pattern spec + * Converts data view saved object to data view spec. * @param savedObject * @returns DataViewSpec */ @@ -586,10 +734,9 @@ export class DataViewsService { }; /** - * Get an index pattern by id. Cache optimized + * 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)); @@ -603,10 +750,10 @@ export class DataViewsService { }; /** - * Create a new index pattern instance - * @param spec - * @param skipFetchFields - * @returns IndexPattern + * Create a new data view instance. + * @param spec data view spec + * @param skipFetchFields if true, will not fetch fields + * @returns DataView */ async create(spec: DataViewSpec, skipFetchFields = false): Promise { const shortDotsEnable = await this.config.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE); @@ -627,8 +774,8 @@ export class DataViewsService { } /** - * Create a new index pattern and save it right away - * @param spec + * Create a new data view and save it right away. + * @param spec data view spec * @param override Overwrite if existing index pattern exists. * @param skipFetchFields Whether to skip field refresh step. */ @@ -641,30 +788,30 @@ export class DataViewsService { } /** - * Save a new index pattern - * @param indexPattern + * Save a new data view. + * @param dataView data view instance * @param override Overwrite if existing index pattern exists */ - async createSavedObject(indexPattern: DataView, override = false) { + async createSavedObject(dataView: DataView, override = false) { if (!(await this.getCanSave())) { throw new DataViewInsufficientAccessError(); } - const dupe = await findByTitle(this.savedObjectsClient, indexPattern.title); + const dupe = await findByTitle(this.savedObjectsClient, dataView.title); if (dupe) { if (override) { await this.delete(dupe.id); } else { - throw new DuplicateDataViewError(`Duplicate index pattern: ${indexPattern.title}`); + throw new DuplicateDataViewError(`Duplicate data view: ${dataView.title}`); } } - const body = indexPattern.getAsSavedObjectBody(); + const body = dataView.getAsSavedObjectBody(); const response: SavedObject = (await this.savedObjectsClient.create( DATA_VIEW_SAVED_OBJECT_TYPE, body, { - id: indexPattern.id, + id: dataView.id, } )) as SavedObject; @@ -677,7 +824,7 @@ export class DataViewsService { } /** - * Save existing index pattern. Will attempt to merge differences if there are conflicts + * Save existing dat aview. Will attempt to merge differences if there are conflicts. * @param indexPattern * @param saveAttempts */ @@ -773,7 +920,7 @@ export class DataViewsService { } /** - * Deletes an index pattern from .kibana index + * Deletes an index pattern from .kibana index. * @param indexPatternId: Id of kibana Index Pattern to delete */ async delete(indexPatternId: string) { @@ -788,7 +935,7 @@ export class DataViewsService { * Returns the default data view as an object. * If no default is found, or it is missing * another data view is selected as default and returned. - * If no possible data view found to become a default returns null + * If no possible data view found to become a default returns null. * * @returns default data view */ @@ -824,13 +971,8 @@ export class DataViewsService { } /** - * @deprecated Use DataViewsService. All index pattern interfaces were renamed. + * Data views service interface + * @public */ -export class IndexPatternsService extends DataViewsService {} export type DataViewsContract = PublicMethodsOf; - -/** - * @deprecated Use DataViewsContract. All index pattern interfaces were renamed. - */ -export type IndexPatternsContract = DataViewsContract; diff --git a/src/plugins/data_views/common/data_views/ensure_default_data_view.ts b/src/plugins/data_views/common/data_views/ensure_default_data_view.ts deleted file mode 100644 index 42e984a3fa88a..0000000000000 --- a/src/plugins/data_views/common/data_views/ensure_default_data_view.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { DataViewsContract } from './data_views'; -import { UiSettingsCommon } from '../types'; - -export type EnsureDefaultDataView = () => Promise | undefined; - -export const createEnsureDefaultDataView = ( - uiSettings: UiSettingsCommon, - onRedirectNoDefaultView: () => Promise | void -) => { - /** - * Checks whether a default data view is set and exists and defines - * one otherwise. - */ - return async function ensureDefaultDataView(this: DataViewsContract) { - if (!(await this.getDefaultDataView())) { - return onRedirectNoDefaultView(); - } - }; -}; diff --git a/src/plugins/data_views/common/data_views/persistable_state.test.ts b/src/plugins/data_views/common/data_views/persistable_state.test.ts new file mode 100644 index 0000000000000..473b9ea02bd47 --- /dev/null +++ b/src/plugins/data_views/common/data_views/persistable_state.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { DataViewPersistableStateService } from './persistable_state'; +import { SavedObjectReference } from '@kbn/core/types'; +import { DataViewSpec } from '../types'; +const { inject, extract } = DataViewPersistableStateService; + +describe('data view persistable state tests', () => { + test('inject references', () => { + const state: DataViewSpec = { + id: 'my-id', + title: 'my-title', + fields: {}, + }; + const references: SavedObjectReference[] = []; + + const result = inject(state, references); + + expect(result).toBe(state); + }); + + test('extract references', () => { + const state: DataViewSpec = { + id: 'my-id', + title: 'my-title', + fields: {}, + }; + + const result = extract(state); + + expect(result.state).toBe(state); + expect(result.references).toEqual([]); + }); +}); diff --git a/src/plugins/data_views/common/data_views/persistable_state.ts b/src/plugins/data_views/common/data_views/persistable_state.ts new file mode 100644 index 0000000000000..604cc3777deaa --- /dev/null +++ b/src/plugins/data_views/common/data_views/persistable_state.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 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 { PersistableStateService } from '@kbn/kibana-utils-plugin/common'; +import { DataViewSpec } from '../types'; + +export const DataViewPersistableStateService: PersistableStateService = { + inject: (state, references) => { + return state; + }, + extract: (state) => { + return { state, references: [] }; + }, + getAllMigrations: () => ({}), + migrateToLatest: ({ state }) => state, + telemetry: () => ({}), +}; diff --git a/src/plugins/data_views/common/errors/data_view_saved_object_conflict.ts b/src/plugins/data_views/common/errors/data_view_saved_object_conflict.ts index 3fcb281655727..f1c9ecaee5026 100644 --- a/src/plugins/data_views/common/errors/data_view_saved_object_conflict.ts +++ b/src/plugins/data_views/common/errors/data_view_saved_object_conflict.ts @@ -6,7 +6,14 @@ * Side Public License, v 1. */ +/** + * Error thrown when saved object has been changed when attempting to save. + */ export class DataViewSavedObjectConflictError extends Error { + /** + * constructor + * @param savedObjectId saved object id with conflict + */ constructor(savedObjectId: string) { super(`Conflict loading DataView saved object, id: ${savedObjectId}`); this.name = 'DataViewSavedObjectConflictError'; diff --git a/src/plugins/data_views/common/errors/duplicate_index_pattern.ts b/src/plugins/data_views/common/errors/duplicate_index_pattern.ts index 942c104eee4e5..455ee0cd1a3eb 100644 --- a/src/plugins/data_views/common/errors/duplicate_index_pattern.ts +++ b/src/plugins/data_views/common/errors/duplicate_index_pattern.ts @@ -6,7 +6,15 @@ * Side Public License, v 1. */ +/** + * Error thrown when attempting to create duplicate index pattern based on title. + * @public + */ export class DuplicateDataViewError extends Error { + /** + * constructor + * @param message - Error message + */ constructor(message: string) { super(message); this.name = 'DuplicateDataViewError'; diff --git a/src/plugins/data_views/common/errors/insufficient_access.ts b/src/plugins/data_views/common/errors/insufficient_access.ts index 48c826ec78557..47cd507cb54e5 100644 --- a/src/plugins/data_views/common/errors/insufficient_access.ts +++ b/src/plugins/data_views/common/errors/insufficient_access.ts @@ -6,7 +6,16 @@ * Side Public License, v 1. */ +/** + * Error thrown when action attempted without sufficient access. + * @constructor + * @param {string} message - Saved object id of data view for display in error message + */ export class DataViewInsufficientAccessError extends Error { + /** + * constructor + * @param {string} message - Saved object id of data view for display in error message + */ constructor(savedObjectId?: string) { super(`Operation failed due to insufficient access, id: ${savedObjectId}`); this.name = 'DataViewInsufficientAccessError'; diff --git a/src/plugins/data_views/common/expressions/load_index_pattern.ts b/src/plugins/data_views/common/expressions/load_index_pattern.ts index 19bb928557988..58510bf3a70f9 100644 --- a/src/plugins/data_views/common/expressions/load_index_pattern.ts +++ b/src/plugins/data_views/common/expressions/load_index_pattern.ts @@ -15,8 +15,18 @@ import { DataViewSpec } from '..'; const name = 'indexPatternLoad'; const type = 'index_pattern'; +/** + * Index pattern expression interface + * @public + */ export interface IndexPatternExpressionType { + /** + * Expression type + */ type: typeof type; + /** + * Value - DataViewSpec + */ value: DataViewSpec; } diff --git a/src/plugins/data_views/common/fields/data_view_field.ts b/src/plugins/data_views/common/fields/data_view_field.ts index d581f3dd6927c..e785f2305732a 100644 --- a/src/plugins/data_views/common/fields/data_view_field.ts +++ b/src/plugins/data_views/common/fields/data_view_field.ts @@ -8,8 +8,8 @@ import { KbnFieldType, getKbnFieldType } from '@kbn/field-types'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; +import { DataViewFieldBase } from '@kbn/es-query'; import type { RuntimeFieldSpec } from '../types'; -import type { IFieldType } from './types'; import { FieldSpec, DataView } from '..'; import { shortenDottedString, @@ -19,12 +19,34 @@ import { getDataViewFieldSubtypeNested, } from './utils'; -/** @public */ -export class DataViewField implements IFieldType { +/** + * Optional format getter when serializing a field + * @public + */ +export interface ToSpecConfig { + /** + * Field format getter + */ + getFormatterForField?: DataView['getFormatterForField']; +} + +/** + * Data view field class + * @public + */ +export class DataViewField implements DataViewFieldBase { readonly spec: FieldSpec; // not writable or serialized + /** + * Kbn field type, used mainly for formattering. + */ private readonly kbnFieldType: KbnFieldType; + /** + * DataView constructor + * @constructor + * @param spec Configuration for the field + */ constructor(spec: FieldSpec) { this.spec = { ...spec, type: spec.name === '_source' ? '_source' : spec.type }; @@ -33,20 +55,32 @@ export class DataViewField implements IFieldType { // writable attrs /** - * Count is used for field popularity + * Count is used for field popularity in discover. */ public get count() { return this.spec.count || 0; } + /** + * Set count, which is used for field popularity in discover. + * @param count count number + */ public set count(count: number) { this.spec.count = count; } + /** + * Returns runtime field definition or undefined if field is not runtime field. + */ + public get runtimeField() { return this.spec.runtimeField; } + /** + * Sets runtime field definition or unsets if undefined is provided. + * @param runtimeField runtime field definition + */ public set runtimeField(runtimeField: RuntimeFieldSpec | undefined) { this.spec.runtimeField = runtimeField; } @@ -58,6 +92,10 @@ export class DataViewField implements IFieldType { return this.spec.script; } + /** + * Sets scripted field painless code + * @param script Painless code + */ public set script(script) { this.spec.script = script; } @@ -69,34 +107,59 @@ export class DataViewField implements IFieldType { return this.spec.lang; } + /** + * Sets scripted field langauge. + * @param lang Scripted field language + */ public set lang(lang) { this.spec.lang = lang; } + /** + * Returns custom label if set, otherwise undefined. + */ + public get customLabel() { return this.spec.customLabel; } + /** + * Sets custom label for field, or unsets if passed undefined. + * @param customLabel custom label value + */ public set customLabel(customLabel) { this.spec.customLabel = customLabel; } /** - * Description of field type conflicts across different indices in the same index pattern + * Description of field type conflicts across different indices in the same index pattern. */ public get conflictDescriptions() { return this.spec.conflictDescriptions; } + /** + * Sets conflict descriptions for field. + * @param conflictDescriptions conflict descriptions + */ + public set conflictDescriptions(conflictDescriptions) { this.spec.conflictDescriptions = conflictDescriptions; } // read only attrs + + /** + * Get field name + */ public get name() { return this.spec.name; } + /** + * Gets display name, calcualted based on name, custom label and shortDotsEnable. + */ + public get displayName(): string { return this.spec.customLabel ? this.spec.customLabel @@ -105,30 +168,57 @@ export class DataViewField implements IFieldType { : this.spec.name; } + /** + * Gets field type + */ public get type() { return this.spec.type; } + /** + * Gets ES types as string array + */ + public get esTypes() { return this.spec.esTypes; } + /** + * Returns true if scripted field + */ + public get scripted() { return !!this.spec.scripted; } + /** + * Returns true if field is searchable + */ + public get searchable() { return !!(this.spec.searchable || this.scripted); } + /** + * Returns true if field is aggregatable + */ + public get aggregatable() { return !!(this.spec.aggregatable || this.scripted); } + /** + * Returns true if field is available via doc values + */ + public get readFromDocValues() { return !!(this.spec.readFromDocValues && !this.scripted); } + /** + * Returns field subtype, multi, nested, or undefined if neither + */ + public get subType() { return this.spec.subType; } @@ -140,11 +230,19 @@ export class DataViewField implements IFieldType { return this.spec.isMapped; } + /** + * Returns true if runtime field defined on data view + */ + public get isRuntimeField() { return !this.isMapped && this.runtimeField !== undefined; } // not writable, not serialized + + /** + * Returns true if field is sortable + */ public get sortable() { return ( this.name === '_score' || @@ -152,6 +250,10 @@ export class DataViewField implements IFieldType { ); } + /** + * Returns true if field is filterable + */ + public get filterable() { return ( this.name === '_id' || @@ -160,31 +262,57 @@ export class DataViewField implements IFieldType { ); } + /** + * Returns true if field is visualizable + */ + public get visualizable() { const notVisualizableFieldTypes: string[] = [KBN_FIELD_TYPES.UNKNOWN, KBN_FIELD_TYPES.CONFLICT]; return this.aggregatable && !notVisualizableFieldTypes.includes(this.spec.type); } + /** + * Returns true if field is subtype nested + */ public isSubtypeNested() { return isDataViewFieldSubtypeNested(this); } + /** + * Returns true if field is subtype multi + */ + public isSubtypeMulti() { return isDataViewFieldSubtypeMulti(this); } + /** + * Returns subtype nested data if exists + */ + public getSubtypeNested() { return getDataViewFieldSubtypeNested(this); } + /** + * Returns subtype multi data if exists + */ + public getSubtypeMulti() { return getDataViewFieldSubtypeMulti(this); } + /** + * Deletes count value. Popularity as used by discover + */ + public deleteCount() { delete this.spec.count; } + /** + * JSON version of field + */ public toJSON() { return { count: this.count, @@ -203,11 +331,14 @@ export class DataViewField implements IFieldType { }; } - public toSpec({ - getFormatterForField, - }: { - getFormatterForField?: DataView['getFormatterForField']; - } = {}): FieldSpec { + /** + * Get field in serialized form - fieldspec. + * @param config provide a method to get a field formatter + * @returns field in serialized form - field spec + */ + public toSpec(config: ToSpecConfig = {}): FieldSpec { + const { getFormatterForField } = config; + return { count: this.count, script: this.script, @@ -229,6 +360,10 @@ export class DataViewField implements IFieldType { }; } + /** + * Returns true if composite runtime field + */ + public isRuntimeCompositeSubField() { return this.runtimeField?.type === 'composite'; } diff --git a/src/plugins/data_views/common/fields/field_list.ts b/src/plugins/data_views/common/fields/field_list.ts index c7ef23735d7bd..660f2f8ad2f97 100644 --- a/src/plugins/data_views/common/fields/field_list.ts +++ b/src/plugins/data_views/common/fields/field_list.ts @@ -7,28 +7,72 @@ */ import { findIndex } from 'lodash'; -import { IFieldType } from './types'; import { DataViewField } from './data_view_field'; import { FieldSpec, DataViewFieldMap } from '../types'; import { DataView } from '../data_views'; type FieldMap = Map; +interface ToSpecOptions { + getFormatterForField?: DataView['getFormatterForField']; +} + +/** + * Interface for data view field list which _extends_ the array class. + */ export interface IIndexPatternFieldList extends Array { + /** + * Add field to field list. + * @param field field spec to add field to list + * @returns data view field instance which was added to list + */ add(field: FieldSpec): DataViewField; + /** + * Returns fields as plain array of data view field instances. + */ getAll(): DataViewField[]; + /** + * Get field by name. Optimized, uses map to find field. + * @param name name of field to find + * @returns data view field instance if found, undefined otherwise + */ getByName(name: DataViewField['name']): DataViewField | undefined; + /** + * Get fields by field type. Optimized, uses map to find fields. + * @param type type of field to find + * @returns array of data view field instances if found, empty array otherwise + */ getByType(type: DataViewField['type']): DataViewField[]; - remove(field: IFieldType): void; + /** + * Remove field from field list + * @param field field for removal + */ + remove(field: DataViewField | FieldSpec): void; + /** + * Remove all fields from field list. + */ removeAll(): void; + /** + * Replace all fields in field list with new fields. + * @param specs array of field specs to add to list + */ replaceAll(specs: FieldSpec[]): void; + /** + * Update a field in the list + * @param field field spec to update + */ update(field: FieldSpec): void; - toSpec(options?: { getFormatterForField?: DataView['getFormatterForField'] }): DataViewFieldMap; + /** + * Field list as field spec map by name + * @param options optionally provide a function to get field formatter for fields + * @return map of field specs by name + */ + toSpec(options?: ToSpecOptions): DataViewFieldMap; } -// extending the array class and using a constructor doesn't work well +// Extending the array class and using a constructor doesn't work well // when calling filter and similar so wrapping in a callback. -// to be removed in the future +// To be removed in the future export const fieldList = ( specs: FieldSpec[] = [], shortDotsEnable = false @@ -43,7 +87,8 @@ export const fieldList = ( } this.groups.get(field.type)!.set(field.name, field); }; - private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name); + private removeByGroup = (field: DataViewField) => + this.groups.get(field.type)?.delete(field.name); constructor() { super(); @@ -63,7 +108,7 @@ export const fieldList = ( return newField; }; - public readonly remove = (field: IFieldType) => { + public readonly remove = (field: DataViewField) => { this.removeByGroup(field); this.byName.delete(field.name); diff --git a/src/plugins/data_views/common/fields/index.ts b/src/plugins/data_views/common/fields/index.ts index 97cbe862d5fe7..6304aeb74bda2 100644 --- a/src/plugins/data_views/common/fields/index.ts +++ b/src/plugins/data_views/common/fields/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export * from './types'; export { isFilterable, isNestedField, diff --git a/src/plugins/data_views/common/fields/types.ts b/src/plugins/data_views/common/fields/types.ts deleted file mode 100644 index b68f5db4f2cdc..0000000000000 --- a/src/plugins/data_views/common/fields/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { DataViewFieldBase } from '@kbn/es-query'; -import { FieldSpec, DataView } from '..'; - -/** - * @deprecated Use {@link IndexPatternField} - * @removeBy 8.2 - */ -export interface IFieldType extends DataViewFieldBase { - count?: number; - // esTypes might be undefined on old index patterns that have not been refreshed since we added - // this prop. It is also undefined on scripted fields. - esTypes?: string[]; - aggregatable?: boolean; - filterable?: boolean; - searchable?: boolean; - sortable?: boolean; - visualizable?: boolean; - readFromDocValues?: boolean; - displayName?: string; - customLabel?: string; - format?: any; - toSpec?: (options?: { getFormatterForField?: DataView['getFormatterForField'] }) => FieldSpec; -} diff --git a/src/plugins/data_views/common/fields/utils.ts b/src/plugins/data_views/common/fields/utils.ts index adb5057798b1c..1dc7e7f698995 100644 --- a/src/plugins/data_views/common/fields/utils.ts +++ b/src/plugins/data_views/common/fields/utils.ts @@ -8,11 +8,11 @@ import { getFilterableKbnTypeNames } from '@kbn/field-types'; import { DataViewFieldBase, IFieldSubTypeNested, IFieldSubTypeMulti } from '@kbn/es-query'; -import { IFieldType } from './types'; +import type { DataViewField } from '.'; const filterableTypes = getFilterableKbnTypeNames(); -export function isFilterable(field: IFieldType): boolean { +export function isFilterable(field: DataViewField): boolean { return ( field.name === '_id' || field.scripted || @@ -55,6 +55,12 @@ export function isDataViewFieldSubtypeMulti(field: HasSubtype) { return !!subTypeNested?.multi?.parent; } +/** + * Returns subtype data for multi field + * @public + * @param field field to get subtype data from + */ + export function getDataViewFieldSubtypeMulti(field: HasSubtype) { return isDataViewFieldSubtypeMulti(field) ? (field.subType as IFieldSubTypeMulti) : undefined; } diff --git a/src/plugins/data_views/common/index.ts b/src/plugins/data_views/common/index.ts index d6ad6b6c44cb2..dd707f6bc7623 100644 --- a/src/plugins/data_views/common/index.ts +++ b/src/plugins/data_views/common/index.ts @@ -12,7 +12,9 @@ export { META_FIELDS, DATA_VIEW_SAVED_OBJECT_TYPE, } from './constants'; -export type { IFieldType, IIndexPatternFieldList } from './fields'; + +export type { ToSpecConfig } from './fields'; +export type { IIndexPatternFieldList } from './fields'; export { isFilterable, fieldList, @@ -29,13 +31,9 @@ export type { RuntimeFieldSpec, RuntimeFieldSubField, DataViewAttributes, - IndexPatternAttributes, - FieldAttrs, - FieldAttrSet, OnNotification, OnError, UiSettingsCommon, - SavedObjectsClientCommonFindArgs, SavedObjectsClientCommon, GetFieldsOptions, IDataViewsApiClient, @@ -43,16 +41,22 @@ export type { AggregationRestrictions, TypeMeta, FieldSpecConflictDescriptions, - FieldSpecExportFmt, FieldSpec, DataViewFieldMap, DataViewSpec, SourceFilter, HasDataService, + RuntimeTypeExceptComposite, + RuntimeFieldBase, + FieldConfiguration, + SavedObjectsClientCommonFindArgs, + FieldAttrs, + FieldAttrSet, } from './types'; export { DataViewType } from './types'; -export type { IndexPatternsContract, DataViewsContract } from './data_views'; -export { IndexPatternsService, DataViewsService } from './data_views'; + +export type { DataViewsContract, DataViewsServiceDeps } from './data_views'; +export { DataViewsService } from './data_views'; export type { DataViewListItem, DataViewsServicePublicMethods, diff --git a/src/plugins/data_views/common/lib/errors.ts b/src/plugins/data_views/common/lib/errors.ts index c75ad7fe29930..29f9e01582f7c 100644 --- a/src/plugins/data_views/common/lib/errors.ts +++ b/src/plugins/data_views/common/lib/errors.ts @@ -22,9 +22,3 @@ export class DataViewMissingIndices extends KbnError { ); } } - -/** - * @deprecated Use DataViewMissingIndices. All index pattern interfaces were renamed. - */ - -export class IndexPatternMissingIndices extends DataViewMissingIndices {} diff --git a/src/plugins/data_views/common/lib/types.ts b/src/plugins/data_views/common/lib/types.ts index bdc5479e97831..58c370045e1e2 100644 --- a/src/plugins/data_views/common/lib/types.ts +++ b/src/plugins/data_views/common/lib/types.ts @@ -6,7 +6,19 @@ * Side Public License, v 1. */ +/** + * Error code for when an index pattern contains illegal characters + */ export const ILLEGAL_CHARACTERS_KEY = 'ILLEGAL_CHARACTERS'; +/** + * Error code for when an index pattern contains spaces + */ export const CONTAINS_SPACES_KEY = 'CONTAINS_SPACES'; +/** + * Characters disallowed in index patterns that are visible. + */ export const ILLEGAL_CHARACTERS_VISIBLE = ['\\', '/', '?', '"', '<', '>', '|']; +/** + * All characters disallowed in index patterns. + */ export const ILLEGAL_CHARACTERS = ILLEGAL_CHARACTERS_VISIBLE.concat(' '); diff --git a/src/plugins/data_views/common/lib/validate_data_view.ts b/src/plugins/data_views/common/lib/validate_data_view.ts index f86ba28e7cde4..85793e169fbb8 100644 --- a/src/plugins/data_views/common/lib/validate_data_view.ts +++ b/src/plugins/data_views/common/lib/validate_data_view.ts @@ -23,6 +23,13 @@ function findIllegalCharacters(indexPattern: string): string[] { return illegalCharacters; } +/** + * Validate index pattern strings + * @public + * @param indexPattern string to validate + * @returns errors object + */ + export function validateDataView(indexPattern: string) { const errors: { [ILLEGAL_CHARACTERS_KEY]?: string[]; [CONTAINS_SPACES_KEY]?: boolean } = {}; diff --git a/src/plugins/data_views/common/lib/validate_index_pattern.test.ts b/src/plugins/data_views/common/lib/validate_index_pattern.test.ts new file mode 100644 index 0000000000000..edf20440931e3 --- /dev/null +++ b/src/plugins/data_views/common/lib/validate_index_pattern.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_KEY, ILLEGAL_CHARACTERS_VISIBLE } from './types'; + +import { validateDataView } from './validate_data_view'; + +describe('Index Pattern Utils', () => { + describe('Validation', () => { + it('should not allow space in the pattern', () => { + const errors = validateDataView('my pattern'); + expect(errors[CONTAINS_SPACES_KEY]).toBe(true); + }); + + it('should not allow illegal characters', () => { + ILLEGAL_CHARACTERS_VISIBLE.forEach((char) => { + const errors = validateDataView(`pattern${char}`); + expect(errors[ILLEGAL_CHARACTERS_KEY]).toEqual([char]); + }); + }); + + it('should return empty object when there are no errors', () => { + expect(validateDataView('my-pattern-*')).toEqual({}); + }); + }); +}); diff --git a/src/plugins/data_views/common/lib/validate_index_pattern.ts b/src/plugins/data_views/common/lib/validate_index_pattern.ts new file mode 100644 index 0000000000000..30c46cb79f51f --- /dev/null +++ b/src/plugins/data_views/common/lib/validate_index_pattern.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ILLEGAL_CHARACTERS_VISIBLE, CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_KEY } from './types'; + +function indexPatternContainsSpaces(indexPattern: string): boolean { + return indexPattern.includes(' '); +} + +function findIllegalCharacters(indexPattern: string): string[] { + const illegalCharacters = ILLEGAL_CHARACTERS_VISIBLE.reduce((chars: string[], char: string) => { + if (indexPattern.includes(char)) { + chars.push(char); + } + return chars; + }, []); + + return illegalCharacters; +} + +/** + * Validates index pattern strings + * @param indexPattern + * @returns Object with validation errors + */ + +export function validateIndexPattern(indexPattern: string) { + const errors: { [ILLEGAL_CHARACTERS_KEY]?: string[]; [CONTAINS_SPACES_KEY]?: boolean } = {}; + + const illegalCharacters = findIllegalCharacters(indexPattern); + + if (illegalCharacters.length) { + errors[ILLEGAL_CHARACTERS_KEY] = illegalCharacters; + } + + if (indexPatternContainsSpaces(indexPattern)) { + errors[CONTAINS_SPACES_KEY] = true; + } + + return errors; +} diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index c85f07fb3c16b..8e6a030380d46 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -5,35 +5,58 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { DataViewFieldBase, IFieldSubType } from '@kbn/es-query'; + +import type { DataViewFieldBase } from '@kbn/es-query'; import { ToastInputFields, ErrorToastOptions } from '@kbn/core/public/notifications'; // eslint-disable-next-line import type { SavedObject } from 'src/core/server'; -import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; +import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import { RUNTIME_FIELD_TYPES } from './constants'; export type { QueryDslQueryContainer }; export type FieldFormatMap = Record; +/** + * Runtime field - type of value returned + * @public + */ + export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; +/** + * Primitive runtime field types + * @public + */ + export type RuntimeTypeExceptComposite = Exclude; -export interface RuntimeFieldBase { +/** + * Runtime field definition + * @public + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type RuntimeFieldBase = { + /** + * Type of runtime field + */ type: RuntimeType; + /** + * Runtime field script + */ script?: { + /** + * Script source + */ source: string; }; -} +}; /** * The RuntimeField that will be sent in the ES Query "runtime_mappings" object */ -export interface RuntimeFieldSpec extends RuntimeFieldBase { +export type RuntimeFieldSpec = RuntimeFieldBase & { fields?: Record< string, { @@ -41,97 +64,238 @@ export interface RuntimeFieldSpec extends RuntimeFieldBase { type: RuntimeTypeExceptComposite; } >; -} +}; +/** + * Field attributes that are user configurable + * @public + */ export interface FieldConfiguration { + /** + * Field format in serialized form + */ format?: SerializedFieldFormat | null; + /** + * Custom label + */ customLabel?: string; + /** + * Popularity - used for discover + */ popularity?: number; } /** * This is the RuntimeField interface enhanced with Data view field * configuration: field format definition, customLabel or popularity. - * - * @see {@link RuntimeField} + * @public */ export interface RuntimeField extends RuntimeFieldBase, FieldConfiguration { + /** + * Subfields of composite field + */ fields?: Record; } +/** + * Runtime field composite subfield + * @public + */ export interface RuntimeFieldSubField extends FieldConfiguration { + /** + * Type of runtime field, can only be primitive type + */ type: RuntimeTypeExceptComposite; } /** - * Interface for an index pattern saved object + * Interface for the data view saved object + * @public */ export interface DataViewAttributes { + /** + * Fields as a serialized array of field specs + */ fields: string; + /** + * Data view title + */ title: string; + /** + * Data view type, default or rollup + */ type?: string; + /** + * Type metadata information, serialized. Only used by rollup data views. + */ typeMeta?: string; + /** + * Time field name + */ timeFieldName?: string; + /** + * Serialized array of filters. Used by discover to hide fields. + */ sourceFilters?: string; + /** + * Serialized map of field formats by field name + */ fieldFormatMap?: string; + /** + * Serialized map of field attributes, currently field count and name + */ fieldAttrs?: string; + /** + * Serialized map of runtime field definitions, by field name + */ runtimeFieldMap?: string; /** - * prevents errors when index pattern exists before indices + * Prevents errors when index pattern exists before indices */ allowNoIndex?: boolean; } /** - * @deprecated Use DataViewAttributes. All index pattern interfaces were renamed. - */ -export type IndexPatternAttributes = DataViewAttributes; - -/** - * @intenal + * Set of field attributes + * @public * Storage of field attributes. Necessary since the field list isn't saved. */ -export interface FieldAttrs { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type FieldAttrs = { [key: string]: FieldAttrSet; -} +}; -export interface FieldAttrSet { +/** + * Field attributes that are stored on the data view + * @public + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type FieldAttrSet = { + /** + * Custom field label + */ customLabel?: string; + /** + * Popularity count - used for discover + */ count?: number; -} +}; +/** + * Handler for data view notifications + * @public + * @param toastInputFields Toast notif config + * @param key Used to dedupe notifs + */ export type OnNotification = (toastInputFields: ToastInputFields, key: string) => void; + +/** + * Handler for data view errors + * @public + * @param error Error object + * @param toastInputFields Toast notif config + * @param key Used to dedupe notifs + */ export type OnError = (error: Error, toastInputFields: ErrorToastOptions, key: string) => void; +/** + * Interface for UiSettings common interface {@link UiSettingsClient} + */ export interface UiSettingsCommon { + /** + * Get a setting value + * @param key name of value + */ get: (key: string) => Promise; + /** + * Get all settings values + */ getAll: () => Promise>; - set: (key: string, value: any) => Promise; + /** + * Set a setting value + * @param key name of value + * @param value value to set + */ + set: (key: string, value: T) => Promise; + /** + * Remove a setting value + * @param key name of value + */ remove: (key: string) => Promise; } +/** + * Saved objects common find args + * @public + */ export interface SavedObjectsClientCommonFindArgs { + /** + * Saved object type + */ type: string | string[]; + /** + * Saved object fields + */ fields?: string[]; + /** + * Results per page + */ perPage?: number; + /** + * Query string + */ search?: string; + /** + * Fields to search + */ searchFields?: string[]; } +/** + * Common interface for the saved objects client + * @public + */ export interface SavedObjectsClientCommon { + /** + * Search for saved objects + * @param options - options for search + */ find: (options: SavedObjectsClientCommonFindArgs) => Promise>>; + /** + * Get a single saved object by id + * @param type - type of saved object + * @param id - id of saved object + */ get: (type: string, id: string) => Promise>; + /** + * Update a saved object by id + * @param type - type of saved object + * @param id - id of saved object + * @param attributes - attributes to update + * @param options - client options + */ update: ( type: string, id: string, attributes: Record, options: Record ) => Promise; + /** + * Create a saved object + * @param type - type of saved object + * @param attributes - attributes to set + * @param options - client options + */ create: ( type: string, attributes: Record, options: Record ) => Promise; + /** + * Delete a saved object by id + * @param type - type of saved object + * @param id - id of saved object + */ delete: (type: string, id: string) => Promise<{}>; } @@ -164,100 +328,160 @@ export type AggregationRestrictions = Record< } >; -export interface TypeMeta { +/** + * Interface for metadata about rollup indices + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type TypeMeta = { + /** + * Aggregation restrictions for rollup fields + */ aggs?: Record; + /** + * Params for retrieving rollup field data + */ params?: { + /** + * Rollup index name used for loading field list + */ rollup_index: string; }; -} +}; +/** + * Data View type. Default or rollup + */ export enum DataViewType { DEFAULT = 'default', ROLLUP = 'rollup', } -/** - * @deprecated Use DataViewType. All index pattern interfaces were renamed. - */ -export enum IndexPatternType { - DEFAULT = DataViewType.DEFAULT, - ROLLUP = DataViewType.ROLLUP, -} - export type FieldSpecConflictDescriptions = Record; -// This should become FieldSpec once types are cleaned up -export interface FieldSpecExportFmt { - count?: number; - script?: string; - lang?: estypes.ScriptLanguage; - conflictDescriptions?: FieldSpecConflictDescriptions; - name: string; - type: KBN_FIELD_TYPES; - esTypes?: string[]; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues?: boolean; - subType?: IFieldSubType; - format?: SerializedFieldFormat; - indexed?: boolean; -} - /** + * Serialized version of DataViewField * @public - * Serialized version of IndexPatternField */ -export interface FieldSpec extends DataViewFieldBase { +export type FieldSpec = DataViewFieldBase & { /** * Popularity count is used by discover */ count?: number; + /** + * Description of field type conflicts across indices + */ conflictDescriptions?: Record; + /** + * Field formatting in serialized format + */ format?: SerializedFieldFormat; + /** + * Elasticsearch field types used by backing indices + */ esTypes?: string[]; + /** + * True if field is searchable + */ searchable: boolean; + /** + * True if field is aggregatable + */ aggregatable: boolean; + /** + * True if can be read from doc values + */ readFromDocValues?: boolean; + /** + * True if field is indexed + */ indexed?: boolean; + /** + * Custom label for field, used for display in kibana + */ customLabel?: string; + /** + * Runtime field definition + */ runtimeField?: RuntimeFieldSpec; + // not persisted + + /** + * Whether short dots are enabled, based on uiSettings. + */ shortDotsEnable?: boolean; + /** + * Is this field in the mapping? False if a scripted or runtime field defined on the data view. + */ isMapped?: boolean; -} +}; export type DataViewFieldMap = Record; /** - * Static index pattern format - * Serialized data object, representing index pattern attributes and state + * Static data view format + * Serialized data object, representing data view attributes and state */ -export interface DataViewSpec { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DataViewSpec = { /** - * saved object id + * Saved object id */ id?: string; /** - * saved object version string + * Saved object version string */ version?: string; + /** + * Data view title + */ title?: string; + /** + * Name of timestamp field + */ timeFieldName?: string; + /** + * List of filters which discover uses to hide fields + */ sourceFilters?: SourceFilter[]; + /** + * Map of fields by name + */ fields?: DataViewFieldMap; + /** + * Metadata about data view, only used by rollup data views + */ typeMeta?: TypeMeta; + /** + * Default or rollup + */ type?: string; + /** + * Map of serialized field formats by field name + */ fieldFormats?: Record; + /** + * Map of runtime fields by field name + */ runtimeFieldMap?: Record; + /** + * Map of field attributes by field name, currently customName and count + */ fieldAttrs?: FieldAttrs; + /** + * Determines whether failure to load field list should be reported as error + */ allowNoIndex?: boolean; + /** + * Array of namespace ids + */ namespaces?: string[]; -} +}; -export interface SourceFilter { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SourceFilter = { value: string; -} +}; export interface HasDataService { hasESData: () => Promise; diff --git a/src/plugins/data_views/common/utils.test.ts b/src/plugins/data_views/common/utils.test.ts index 51c5e99be50db..3351a15da1a13 100644 --- a/src/plugins/data_views/common/utils.test.ts +++ b/src/plugins/data_views/common/utils.test.ts @@ -7,43 +7,47 @@ */ import { isFilterable } from '.'; -import { IFieldType } from './fields'; +import type { DataViewField } from './fields'; const mockField = { name: 'foo', scripted: false, searchable: true, type: 'string', -} as IFieldType; +} as DataViewField; describe('isFilterable', () => { describe('types', () => { it('should return true for filterable types', () => { ['string', 'number', 'date', 'ip', 'boolean'].forEach((type) => { - expect(isFilterable({ ...mockField, type })).toBe(true); + expect(isFilterable({ ...mockField, type } as DataViewField)).toBe(true); }); }); it('should return false for filterable types if the field is not searchable', () => { ['string', 'number', 'date', 'ip', 'boolean'].forEach((type) => { - expect(isFilterable({ ...mockField, type, searchable: false })).toBe(false); + expect(isFilterable({ ...mockField, type, searchable: false } as DataViewField)).toBe( + false + ); }); }); it('should return false for un-filterable types', () => { ['geo_point', 'geo_shape', 'attachment', 'murmur3', '_source', 'unknown', 'conflict'].forEach( (type) => { - expect(isFilterable({ ...mockField, type })).toBe(false); + expect(isFilterable({ ...mockField, type } as DataViewField)).toBe(false); } ); }); }); it('should return true for scripted fields', () => { - expect(isFilterable({ ...mockField, scripted: true, searchable: false })).toBe(true); + expect(isFilterable({ ...mockField, scripted: true, searchable: false } as DataViewField)).toBe( + true + ); }); it('should return true for the _id field', () => { - expect(isFilterable({ ...mockField, name: '_id' })).toBe(true); + expect(isFilterable({ ...mockField, name: '_id' } as DataViewField)).toBe(true); }); }); diff --git a/src/plugins/data_views/common/utils.ts b/src/plugins/data_views/common/utils.ts index 77e9bd76b869c..98f55d6265d27 100644 --- a/src/plugins/data_views/common/utils.ts +++ b/src/plugins/data_views/common/utils.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { IndexPatternSavedObjectAttrs } from './data_views'; +import type { DataViewSavedObjectAttrs } from './data_views'; import type { SavedObjectsClientCommon } from './types'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from './constants'; @@ -20,7 +20,7 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from './constants'; */ export async function findByTitle(client: SavedObjectsClientCommon, title: string) { if (title) { - const savedObjects = await client.find({ + const savedObjects = await client.find({ type: DATA_VIEW_SAVED_OBJECT_TYPE, perPage: 10, search: `"${title}"`, diff --git a/src/plugins/data_views/kibana.json b/src/plugins/data_views/kibana.json index 04d5e93100e40..f92eb6bad2aed 100644 --- a/src/plugins/data_views/kibana.json +++ b/src/plugins/data_views/kibana.json @@ -6,7 +6,7 @@ "requiredPlugins": ["fieldFormats", "expressions"], "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], - "requiredBundles": ["kibanaUtils", "kibanaReact"], + "requiredBundles": ["kibanaUtils"], "owner": { "name": "App Services", "githubTeam": "kibana-app-services" diff --git a/src/plugins/data_views/public/data_views/data_views_api_client.ts b/src/plugins/data_views/public/data_views/data_views_api_client.ts index fdf24e21c2e01..e1b7ee1590acf 100644 --- a/src/plugins/data_views/public/data_views/data_views_api_client.ts +++ b/src/plugins/data_views/public/data_views/data_views_api_client.ts @@ -12,9 +12,16 @@ import { GetFieldsOptions, IDataViewsApiClient } from '../../common'; const API_BASE_URL: string = `/api/index_patterns/`; +/** + * Data Views API Client - client implementation + */ export class DataViewsApiClient implements IDataViewsApiClient { private http: HttpSetup; + /** + * constructor + * @param http http dependency + */ constructor(http: HttpSetup) { this.http = http; } @@ -37,14 +44,12 @@ export class DataViewsApiClient implements IDataViewsApiClient { return API_BASE_URL + path.filter(Boolean).map(encodeURIComponent).join('/'); } - getFieldsForWildcard({ - pattern, - metaFields, - type, - rollupIndex, - allowNoIndex, - filter, - }: GetFieldsOptions) { + /** + * Get field list for a given index pattern + * @param options options for fields request + */ + getFieldsForWildcard(options: GetFieldsOptions) { + const { pattern, metaFields, type, rollupIndex, allowNoIndex, filter } = options; return this._request(this._getUrl(['_fields_for_wildcard']), { pattern, meta_fields: metaFields, @@ -55,6 +60,9 @@ export class DataViewsApiClient implements IDataViewsApiClient { }).then((resp: any) => resp.fields || []); } + /** + * Does a user created data view exist? + */ async hasUserIndexPattern(): Promise { const response = await this._request<{ result: boolean }>( this._getUrl(['has_user_index_pattern']) diff --git a/src/plugins/data_views/public/data_views/index.ts b/src/plugins/data_views/public/data_views/index.ts index e476d62774f17..f424dd7161153 100644 --- a/src/plugins/data_views/public/data_views/index.ts +++ b/src/plugins/data_views/public/data_views/index.ts @@ -7,5 +7,4 @@ */ export * from '../../common/data_views'; -export * from './redirect_no_index_pattern'; export * from './data_views_api_client'; diff --git a/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx b/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx deleted file mode 100644 index 3ddfd24f687af..0000000000000 --- a/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { CoreStart } from '@kbn/core/public'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; - -let bannerId: string; - -export const onRedirectNoIndexPattern = - ( - capabilities: CoreStart['application']['capabilities'], - navigateToApp: CoreStart['application']['navigateToApp'], - overlays: CoreStart['overlays'], - theme: CoreStart['theme'] - ) => - () => { - const canManageIndexPatterns = capabilities.management.kibana.indexPatterns; - const redirectTarget = canManageIndexPatterns ? '/management/kibana/dataViews' : '/home'; - let timeoutId: NodeJS.Timeout | undefined; - - if (timeoutId) { - clearTimeout(timeoutId); - } - - const bannerMessage = i18n.translate('dataViews.ensureDefaultIndexPattern.bannerLabel', { - defaultMessage: - 'To visualize and explore data in Kibana, you must create an index pattern to retrieve data from Elasticsearch.', - }); - - // Avoid being hostile to new users who don't have an index pattern setup yet - // give them a friendly info message instead of a terse error message - bannerId = overlays.banners.replace( - bannerId, - toMountPoint(, { - theme$: theme.theme$, - }) - ); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - timeoutId = setTimeout(() => { - overlays.banners.remove(bannerId); - timeoutId = undefined; - }, 15000); - - if (redirectTarget === '/home') { - navigateToApp('home'); - } else { - navigateToApp('management', { - path: `/kibana/indexPatterns?bannerMessage=${bannerMessage}`, - }); - } - - // return never-resolving promise to stop resolving and wait for the url change - return new Promise(() => {}); - }; diff --git a/src/plugins/data_views/public/data_views_service_public.ts b/src/plugins/data_views/public/data_views_service_public.ts index ceedffb553b66..4693e7000b2a3 100644 --- a/src/plugins/data_views/public/data_views_service_public.ts +++ b/src/plugins/data_views/public/data_views_service_public.ts @@ -11,15 +11,34 @@ import { DataViewsService } from '.'; import { DataViewsServiceDeps } from '../common/data_views/data_views'; import { HasDataService } from '../common'; -interface DataViewsServicePublicDeps extends DataViewsServiceDeps { +/** + * Data Views public service dependencies + * @public + */ +export interface DataViewsServicePublicDeps extends DataViewsServiceDeps { + /** + * Get can user save data view - sync version + */ getCanSaveSync: () => boolean; + /** + * Has data service + */ hasData: HasDataService; } +/** + * Data Views public service + * @public + */ export class DataViewsServicePublic extends DataViewsService { public getCanSaveSync: () => boolean; public hasData: HasDataService; + /** + * Constructor + * @param deps Service dependencies + */ + constructor(deps: DataViewsServicePublicDeps) { super(deps); this.getCanSaveSync = deps.getCanSaveSync; diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index d4d33a931358d..cf48aaee81fd0 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -13,10 +13,14 @@ export { ILLEGAL_CHARACTERS, validateDataView, } from '../common/lib'; -export { onRedirectNoIndexPattern } from './data_views'; export type { IIndexPatternFieldList, TypeMeta, RuntimeType } from '../common'; -export type { DataViewSpec, FieldSpec, DataViewAttributes } from '../common'; +export type { + DataViewSpec, + FieldSpec, + DataViewAttributes, + SavedObjectsClientCommon, +} from '../common'; export { DataViewField, DataViewType, @@ -27,9 +31,14 @@ export { getFieldSubtypeNested, } from '../common'; -export type { IndexPatternsContract } from './data_views'; +export type { DataViewsPublicSetupDependencies, DataViewsPublicStartDependencies } from './types'; + +export type { + DataViewsServicePublic, + DataViewsServicePublicDeps, +} from './data_views_service_public'; +export { DataViewsApiClient, DataViewsService, DataView } from './data_views'; export type { DataViewListItem } from './data_views'; -export { IndexPatternsService, DataViewsApiClient, DataViewsService, DataView } from './data_views'; export { UiSettingsPublicToCommon } from './ui_settings_wrapper'; export { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; @@ -48,8 +57,6 @@ export type { DataViewsPublicPluginStart, DataViewsContract, HasDataViewsResponse, - IndicesResponse, - IndicesResponseModified, IndicesViaSearchResponse, } from './types'; diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts index 5c3ad2c33307d..1de31618172b6 100644 --- a/src/plugins/data_views/public/plugin.ts +++ b/src/plugins/data_views/public/plugin.ts @@ -15,12 +15,10 @@ import { DataViewsPublicStartDependencies, } from './types'; -import { - onRedirectNoIndexPattern, - DataViewsApiClient, - UiSettingsPublicToCommon, - SavedObjectsClientPublicToCommon, -} from '.'; +import { DataViewsApiClient } from '.'; +import { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; + +import { UiSettingsPublicToCommon } from './ui_settings_wrapper'; import { DataViewsServicePublic } from './data_views_service_public'; import { HasData } from './services'; @@ -51,7 +49,7 @@ export class DataViewsPublicPlugin core: CoreStart, { fieldFormats }: DataViewsPublicStartDependencies ): DataViewsPublicPluginStart { - const { uiSettings, http, notifications, savedObjects, theme, overlays, application } = core; + const { uiSettings, http, notifications, savedObjects, application } = core; const onNotifDebounced = debounceByKey( notifications.toasts.add.bind(notifications.toasts), @@ -74,12 +72,6 @@ export class DataViewsPublicPlugin onError: (error, toastInputFields, key) => { onErrorDebounced(key)(error, toastInputFields); }, - onRedirectNoIndexPattern: onRedirectNoIndexPattern( - application.capabilities, - application.navigateToApp, - overlays, - theme - ), getCanSave: () => Promise.resolve(application.capabilities.indexPatterns.save === true), getCanSaveSync: () => application.capabilities.indexPatterns.save === true, }); diff --git a/src/plugins/data_views/public/saved_objects_client_wrapper.ts b/src/plugins/data_views/public/saved_objects_client_wrapper.ts index c6403cadd04b7..9dab5ec46f548 100644 --- a/src/plugins/data_views/public/saved_objects_client_wrapper.ts +++ b/src/plugins/data_views/public/saved_objects_client_wrapper.ts @@ -12,8 +12,8 @@ import { SavedObjectsClientCommon, SavedObjectsClientCommonFindArgs, SavedObject, - DataViewSavedObjectConflictError, -} from '../common'; +} from '../common/types'; +import { DataViewSavedObjectConflictError } from '../common/errors'; type SOClient = Pick< SavedObjectsClientContract, diff --git a/src/plugins/data_views/public/services/has_data.ts b/src/plugins/data_views/public/services/has_data.ts index d10f6a3d446f8..f9f93e0614c66 100644 --- a/src/plugins/data_views/public/services/has_data.ts +++ b/src/plugins/data_views/public/services/has_data.ts @@ -8,12 +8,8 @@ import { CoreStart, HttpStart } from '@kbn/core/public'; import { DEFAULT_ASSETS_TO_IGNORE } from '../../common'; -import { - HasDataViewsResponse, - IndicesResponse, - IndicesResponseModified, - IndicesViaSearchResponse, -} from '..'; +import { HasDataViewsResponse, IndicesViaSearchResponse } from '..'; +import { IndicesResponse, IndicesResponseModified } from '../types'; export class HasData { private removeAliases = (source: IndicesResponseModified): boolean => !source.item.indices; diff --git a/src/plugins/data_views/public/types.ts b/src/plugins/data_views/public/types.ts index f2d34961ab6e0..fc888d2c42c87 100644 --- a/src/plugins/data_views/public/types.ts +++ b/src/plugins/data_views/public/types.ts @@ -65,12 +65,27 @@ export interface HasDataViewsResponse { hasUserDataView: boolean; } +/** + * Data views public setup dependencies + */ export interface DataViewsPublicSetupDependencies { + /** + * Expressions + */ expressions: ExpressionsSetup; + /** + * Field formats + */ fieldFormats: FieldFormatsSetup; } +/** + * Data views public start dependencies + */ export interface DataViewsPublicStartDependencies { + /** + * Field formats + */ fieldFormats: FieldFormatsStart; } diff --git a/src/plugins/data_views/server/constants.ts b/src/plugins/data_views/server/constants.ts index 7daafe65f9b92..d076435dcf149 100644 --- a/src/plugins/data_views/server/constants.ts +++ b/src/plugins/data_views/server/constants.ts @@ -6,24 +6,73 @@ * Side Public License, v 1. */ +/** + * Service path for data views REST API + */ export const SERVICE_PATH = '/api/data_views'; +/** + * Legacy service path for data views REST API + */ export const SERVICE_PATH_LEGACY = '/api/index_patterns'; +/** + * Path for data view creation + */ export const DATA_VIEW_PATH = `${SERVICE_PATH}/data_view`; +/** + * Legacy path for data view creation + */ export const DATA_VIEW_PATH_LEGACY = `${SERVICE_PATH_LEGACY}/index_pattern`; +/** + * Path for single data view + */ export const SPECIFIC_DATA_VIEW_PATH = `${DATA_VIEW_PATH}/{id}`; +/** + * Legacy path for single data view + */ export const SPECIFIC_DATA_VIEW_PATH_LEGACY = `${DATA_VIEW_PATH_LEGACY}/{id}`; +/** + * Path to create runtime field + */ export const RUNTIME_FIELD_PATH = `${SPECIFIC_DATA_VIEW_PATH}/runtime_field`; +/** + * Legacy path to create runtime field + */ export const RUNTIME_FIELD_PATH_LEGACY = `${SPECIFIC_DATA_VIEW_PATH_LEGACY}/runtime_field`; +/** + * Path for runtime field + */ export const SPECIFIC_RUNTIME_FIELD_PATH = `${RUNTIME_FIELD_PATH}/{name}`; +/** + * Legacy path for runtime field + */ export const SPECIFIC_RUNTIME_FIELD_PATH_LEGACY = `${RUNTIME_FIELD_PATH_LEGACY}/{name}`; +/** + * Path to create scripted field + */ export const SCRIPTED_FIELD_PATH = `${SPECIFIC_DATA_VIEW_PATH}/scripted_field`; +/** + * Legacy path to create scripted field + */ export const SCRIPTED_FIELD_PATH_LEGACY = `${SPECIFIC_DATA_VIEW_PATH_LEGACY}/scripted_field`; +/** + * Path for scripted field + */ export const SPECIFIC_SCRIPTED_FIELD_PATH = `${SCRIPTED_FIELD_PATH}/{name}`; +/** + * Legacy path for scripted field + */ export const SPECIFIC_SCRIPTED_FIELD_PATH_LEGACY = `${SCRIPTED_FIELD_PATH_LEGACY}/{name}`; +/** + * name of service in path form + */ export const SERVICE_KEY = 'data_view'; +/** + * Legacy name of service in path form + */ export const SERVICE_KEY_LEGACY = 'index_pattern'; +/** + * Service keys as type + */ export type SERVICE_KEY_TYPE = typeof SERVICE_KEY | typeof SERVICE_KEY_LEGACY; - -export const CREATE_DATA_VIEW_COUNTER_NAME = `POST ${DATA_VIEW_PATH}`; diff --git a/src/plugins/data_views/server/data_views_service_factory.ts b/src/plugins/data_views/server/data_views_service_factory.ts index 570ced7b62580..f1501b19ca438 100644 --- a/src/plugins/data_views/server/data_views_service_factory.ts +++ b/src/plugins/data_views/server/data_views_service_factory.ts @@ -20,23 +20,25 @@ import { UiSettingsServerToCommon } from './ui_settings_wrapper'; import { IndexPatternsApiServer } from './index_patterns_api_client'; import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; -export const dataViewsServiceFactory = ({ - logger, - uiSettings, - fieldFormats, - capabilities, -}: { +interface DataViewsServiceFactoryDeps { logger: Logger; uiSettings: UiSettingsServiceStart; fieldFormats: FieldFormatsStart; capabilities: CoreStart['capabilities']; -}) => +} + +/** + * Creates a new DataViewsService instance. + * @param deps - Dependencies required by the DataViewsService + */ +export const dataViewsServiceFactory = (deps: DataViewsServiceFactoryDeps) => async function ( savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient, request?: KibanaRequest, byPassCapabilities?: boolean ) { + const { logger, uiSettings, fieldFormats, capabilities } = deps; const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); diff --git a/src/plugins/data_views/server/expressions/load_index_pattern.ts b/src/plugins/data_views/server/expressions/load_index_pattern.ts index 091a6aa2c3817..bfa44f9469da5 100644 --- a/src/plugins/data_views/server/expressions/load_index_pattern.ts +++ b/src/plugins/data_views/server/expressions/load_index_pattern.ts @@ -80,10 +80,10 @@ export function getIndexPatternLoad({ }) { return getFunctionDefinition({ getStartDependencies: async (request: KibanaRequest) => { - const [{ elasticsearch, savedObjects }, , { indexPatternsServiceFactory }] = + const [{ elasticsearch, savedObjects }, , { dataViewsServiceFactory }] = await getStartServices(); return { - indexPatterns: await indexPatternsServiceFactory( + indexPatterns: await dataViewsServiceFactory( savedObjects.getScopedClient(request), elasticsearch.client.asScoped(request).asCurrentUser, request diff --git a/src/plugins/data_views/server/fetcher/lib/map_capabilities.ts b/src/plugins/data_views/server/fetcher/lib/map_capabilities.ts index 19d3c244f4654..e2076a373d489 100644 --- a/src/plugins/data_views/server/fetcher/lib/map_capabilities.ts +++ b/src/plugins/data_views/server/fetcher/lib/map_capabilities.ts @@ -8,6 +8,12 @@ import { mergeJobConfigurations } from './jobs_compatibility'; +/** + * Get rollup job capabilities + * @public + * @param indices rollup job index capabilites + */ + export function getCapabilitiesForRollupIndices(indices: Record) { const indexNames = Object.keys(indices); const capabilities = {} as { [key: string]: any }; diff --git a/src/plugins/data_views/server/has_user_index_pattern.ts b/src/plugins/data_views/server/has_user_index_pattern.ts index b2ef306054582..7473ff6d35d3f 100644 --- a/src/plugins/data_views/server/has_user_index_pattern.ts +++ b/src/plugins/data_views/server/has_user_index_pattern.ts @@ -11,7 +11,7 @@ import { SavedObjectsClientContract, SavedObjectsFindResponse, } from '@kbn/core/server'; -import { IndexPatternSavedObjectAttrs } from '../common/data_views'; +import { DataViewSavedObjectAttrs } from '../common/data_views'; import { DEFAULT_ASSETS_TO_IGNORE } from '../common/constants'; interface Deps { @@ -22,8 +22,8 @@ interface Deps { export const getIndexPattern = async ({ esClient, soClient, -}: Deps): Promise> => - soClient.find({ +}: Deps): Promise> => + soClient.find({ type: 'index-pattern', fields: ['title'], search: `*`, @@ -33,7 +33,7 @@ export const getIndexPattern = async ({ export const hasUserIndexPattern = async ( { esClient, soClient }: Deps, - indexPatterns?: SavedObjectsFindResponse + indexPatterns?: SavedObjectsFindResponse ): Promise => { if (!indexPatterns) { indexPatterns = await getIndexPattern({ esClient, soClient }); diff --git a/src/plugins/data_views/server/index.ts b/src/plugins/data_views/server/index.ts index 6558d75c50a46..e9eb7f0b50a3f 100644 --- a/src/plugins/data_views/server/index.ts +++ b/src/plugins/data_views/server/index.ts @@ -8,13 +8,12 @@ export { getFieldByName, findIndexPatternById } from './utils'; export type { FieldDescriptor } from './fetcher'; -export { - IndexPatternsFetcher, - shouldReadFieldFromDocValues, - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, -} from './fetcher'; -export type { IndexPatternsServiceStart } from './types'; +export { IndexPatternsFetcher, getCapabilitiesForRollupIndices } from './fetcher'; +export type { + DataViewsServerPluginStart, + DataViewsServerPluginSetupDependencies, + DataViewsServerPluginStartDependencies, +} from './types'; import { PluginInitializerContext } from '@kbn/core/server'; import { DataViewsServerPlugin } from './plugin'; @@ -56,3 +55,6 @@ export { } from './constants'; export type { SERVICE_KEY_TYPE } from './constants'; + +export type { FieldSpec, SavedObjectsClientCommon } from '../common/types'; +export { DataViewsService, DataView } from '../common/data_views'; diff --git a/src/plugins/data_views/server/index_patterns_api_client.ts b/src/plugins/data_views/server/index_patterns_api_client.ts index 81003f6bfb60a..0cdcb55a61667 100644 --- a/src/plugins/data_views/server/index_patterns_api_client.ts +++ b/src/plugins/data_views/server/index_patterns_api_client.ts @@ -49,6 +49,9 @@ export class IndexPatternsApiServer implements IDataViewsApiClient { }); } + /** + * Is there a user created data view? + */ async hasUserIndexPattern() { return hasUserIndexPattern({ esClient: this.esClient, diff --git a/src/plugins/data_views/server/mocks.ts b/src/plugins/data_views/server/mocks.ts index 361daf4b937d4..82595f7dc51a1 100644 --- a/src/plugins/data_views/server/mocks.ts +++ b/src/plugins/data_views/server/mocks.ts @@ -11,7 +11,6 @@ import { DataViewsService } from '../common'; export function createIndexPatternsStartMock() { const dataViewsServiceFactory = jest.fn().mockResolvedValue({ get: jest.fn() }); return { - indexPatternsServiceFactory: dataViewsServiceFactory, dataViewsServiceFactory, }; } @@ -29,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/plugin.ts b/src/plugins/data_views/server/plugin.ts index 6ec356a150bd5..9727495553fe0 100644 --- a/src/plugins/data_views/server/plugin.ts +++ b/src/plugins/data_views/server/plugin.ts @@ -65,7 +65,6 @@ export class DataViewsServerPlugin }); return { - indexPatternsServiceFactory: serviceFactory, dataViewsServiceFactory: serviceFactory, }; } diff --git a/src/plugins/data_views/server/register_index_pattern_usage_collection.ts b/src/plugins/data_views/server/register_index_pattern_usage_collection.ts index 9c005d531da23..484a1289a59f6 100644 --- a/src/plugins/data_views/server/register_index_pattern_usage_collection.ts +++ b/src/plugins/data_views/server/register_index_pattern_usage_collection.ts @@ -153,9 +153,9 @@ export function registerIndexPatternsUsageCollector( type: 'index-patterns', isReady: () => true, fetch: async () => { - const [{ savedObjects, elasticsearch }, , { indexPatternsServiceFactory }] = + const [{ savedObjects, elasticsearch }, , { dataViewsServiceFactory }] = await getStartServices(); - const indexPatternService = await indexPatternsServiceFactory( + const indexPatternService = await dataViewsServiceFactory( new SavedObjectsClient(savedObjects.createInternalRepository()), elasticsearch.client.asInternalUser ); diff --git a/src/plugins/data_views/server/rest_api_routes/create_data_view.ts b/src/plugins/data_views/server/rest_api_routes/create_data_view.ts index c35344f54c4fa..4244888a940d9 100644 --- a/src/plugins/data_views/server/rest_api_routes/create_data_view.ts +++ b/src/plugins/data_views/server/rest_api_routes/create_data_view.ts @@ -9,7 +9,8 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { schema } from '@kbn/config-schema'; import { IRouter, StartServicesAccessor } from '@kbn/core/server'; -import { DataViewSpec, DataViewsService } from '../../common'; +import { DataViewSpec } from '../../common/types'; +import { DataViewsService } from '../../common/data_views'; import { handleErrors } from './util/handle_errors'; import { fieldSpecSchema, runtimeFieldSchema, serializedFieldFormatSchema } from './util/schemas'; import type { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from '../types'; diff --git a/src/plugins/data_views/server/rest_api_routes/fields/update_fields.ts b/src/plugins/data_views/server/rest_api_routes/fields/update_fields.ts index d99aa67c7f153..1695f28675188 100644 --- a/src/plugins/data_views/server/rest_api_routes/fields/update_fields.ts +++ b/src/plugins/data_views/server/rest_api_routes/fields/update_fields.ts @@ -9,7 +9,7 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { schema } from '@kbn/config-schema'; import { IRouter, StartServicesAccessor } from '@kbn/core/server'; -import { FieldFormatParams, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; +import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import { DataViewsService } from '../../../common'; import { handleErrors } from '../util/handle_errors'; import { serializedFieldFormatSchema } from '../util/schemas'; @@ -83,7 +83,7 @@ export const updateFields = async ({ interface FieldUpdateType { customLabel?: string | null; count?: number | null; - format?: SerializedFieldFormat | null; + format?: SerializedFieldFormat | null; } const fieldUpdateSchema = schema.object({ 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 0000000000000..216fe58693965 --- /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 0000000000000..f7a77d7e5c8d1 --- /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 3ed0ac6608e1e..812cda62ac1ef 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/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.ts index 5c9faaf2c8593..da3928c874930 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/create_runtime_field.ts @@ -9,7 +9,8 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { schema } from '@kbn/config-schema'; import { IRouter, StartServicesAccessor } from '@kbn/core/server'; -import { DataViewsService, RuntimeField } from '../../../common'; +import { DataViewsService } from '../../../common/data_views'; +import { RuntimeField } from '../../../common/types'; import { handleErrors } from '../util/handle_errors'; import { runtimeFieldSchema } from '../util/schemas'; import type { diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.ts index 867766eec3124..f38c232c247f4 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/get_runtime_field.ts @@ -9,7 +9,7 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { schema } from '@kbn/config-schema'; import { IRouter, StartServicesAccessor } from '@kbn/core/server'; -import { DataViewsService } from '../../../common'; +import { DataViewsService } from '../../../common/data_views'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; import type { diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/put_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/put_runtime_field.ts index 0f5399606dfdc..98378da328410 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/put_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/put_runtime_field.ts @@ -9,7 +9,8 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { schema } from '@kbn/config-schema'; import { IRouter, StartServicesAccessor } from '@kbn/core/server'; -import { DataViewsService, RuntimeField } from '../../../common'; +import { DataViewsService } from '../../../common/data_views'; +import { RuntimeField } from '../../../common/types'; import { handleErrors } from '../util/handle_errors'; import { runtimeFieldSchema } from '../util/schemas'; import type { diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.ts index 1aaf1b112feed..880b4bc59b601 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/update_runtime_field.ts @@ -9,7 +9,8 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { schema } from '@kbn/config-schema'; import { IRouter, StartServicesAccessor } from '@kbn/core/server'; -import { DataViewsService, RuntimeField } from '../../../common'; +import { DataViewsService } from '../../../common/data_views'; +import { RuntimeField } from '../../../common/types'; import { ErrorIndexPatternFieldNotFound } from '../../error'; import { handleErrors } from '../util/handle_errors'; import { runtimeFieldSchema } from '../util/schemas'; diff --git a/src/plugins/data_views/server/rest_api_routes/scripted_fields/create_scripted_field.ts b/src/plugins/data_views/server/rest_api_routes/scripted_fields/create_scripted_field.ts index 759627f926762..a4a38c056d825 100644 --- a/src/plugins/data_views/server/rest_api_routes/scripted_fields/create_scripted_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/scripted_fields/create_scripted_field.ts @@ -45,8 +45,8 @@ export const registerCreateScriptedFieldRoute = ( const core = await ctx.core; const savedObjectsClient = core.savedObjects.client; const elasticsearchClient = core.elasticsearch.client.asCurrentUser; - const [, , { indexPatternsServiceFactory }] = await getStartServices(); - const indexPatternsService = await indexPatternsServiceFactory( + const [, , { dataViewsServiceFactory }] = await getStartServices(); + const indexPatternsService = await dataViewsServiceFactory( savedObjectsClient, elasticsearchClient, req diff --git a/src/plugins/data_views/server/rest_api_routes/scripted_fields/delete_scripted_field.ts b/src/plugins/data_views/server/rest_api_routes/scripted_fields/delete_scripted_field.ts index 7e3333820e4e9..e381eb8f09bf3 100644 --- a/src/plugins/data_views/server/rest_api_routes/scripted_fields/delete_scripted_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/scripted_fields/delete_scripted_field.ts @@ -46,8 +46,8 @@ export const registerDeleteScriptedFieldRoute = ( const core = await ctx.core; const savedObjectsClient = core.savedObjects.client; const elasticsearchClient = core.elasticsearch.client.asCurrentUser; - const [, , { indexPatternsServiceFactory }] = await getStartServices(); - const indexPatternsService = await indexPatternsServiceFactory( + const [, , { dataViewsServiceFactory }] = await getStartServices(); + const indexPatternsService = await dataViewsServiceFactory( savedObjectsClient, elasticsearchClient, req diff --git a/src/plugins/data_views/server/rest_api_routes/scripted_fields/get_scripted_field.ts b/src/plugins/data_views/server/rest_api_routes/scripted_fields/get_scripted_field.ts index befe30f8437f2..e5cf849205a55 100644 --- a/src/plugins/data_views/server/rest_api_routes/scripted_fields/get_scripted_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/scripted_fields/get_scripted_field.ts @@ -46,8 +46,8 @@ export const registerGetScriptedFieldRoute = ( const core = await ctx.core; const savedObjectsClient = core.savedObjects.client; const elasticsearchClient = core.elasticsearch.client.asCurrentUser; - const [, , { indexPatternsServiceFactory }] = await getStartServices(); - const indexPatternsService = await indexPatternsServiceFactory( + const [, , { dataViewsServiceFactory }] = await getStartServices(); + const indexPatternsService = await dataViewsServiceFactory( savedObjectsClient, elasticsearchClient, req diff --git a/src/plugins/data_views/server/rest_api_routes/scripted_fields/put_scripted_field.ts b/src/plugins/data_views/server/rest_api_routes/scripted_fields/put_scripted_field.ts index 93312dd6d3cf6..e42c364fac8f0 100644 --- a/src/plugins/data_views/server/rest_api_routes/scripted_fields/put_scripted_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/scripted_fields/put_scripted_field.ts @@ -45,8 +45,8 @@ export const registerPutScriptedFieldRoute = ( const core = await ctx.core; const savedObjectsClient = core.savedObjects.client; const elasticsearchClient = core.elasticsearch.client.asCurrentUser; - const [, , { indexPatternsServiceFactory }] = await getStartServices(); - const indexPatternsService = await indexPatternsServiceFactory( + const [, , { dataViewsServiceFactory }] = await getStartServices(); + const indexPatternsService = await dataViewsServiceFactory( savedObjectsClient, elasticsearchClient, req diff --git a/src/plugins/data_views/server/rest_api_routes/scripted_fields/update_scripted_field.ts b/src/plugins/data_views/server/rest_api_routes/scripted_fields/update_scripted_field.ts index ddc9d7ae552e8..642761a61b7cb 100644 --- a/src/plugins/data_views/server/rest_api_routes/scripted_fields/update_scripted_field.ts +++ b/src/plugins/data_views/server/rest_api_routes/scripted_fields/update_scripted_field.ts @@ -66,8 +66,8 @@ export const registerUpdateScriptedFieldRoute = ( const core = await ctx.core; const savedObjectsClient = core.savedObjects.client; const elasticsearchClient = core.elasticsearch.client.asCurrentUser; - const [, , { indexPatternsServiceFactory }] = await getStartServices(); - const indexPatternsService = await indexPatternsServiceFactory( + const [, , { dataViewsServiceFactory }] = await getStartServices(); + const indexPatternsService = await dataViewsServiceFactory( savedObjectsClient, elasticsearchClient, req diff --git a/src/plugins/data_views/server/rest_api_routes/update_data_view.ts b/src/plugins/data_views/server/rest_api_routes/update_data_view.ts index 424680f85b498..22598a8251096 100644 --- a/src/plugins/data_views/server/rest_api_routes/update_data_view.ts +++ b/src/plugins/data_views/server/rest_api_routes/update_data_view.ts @@ -9,7 +9,8 @@ import { schema } from '@kbn/config-schema'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { IRouter, StartServicesAccessor } from '@kbn/core/server'; -import { DataViewSpec, DataViewsService } from '../../common'; +import { DataViewsService } from '../../common/data_views'; +import { DataViewSpec } from '../../common/types'; import { handleErrors } from './util/handle_errors'; import { fieldSpecSchema, runtimeFieldSchema, serializedFieldFormatSchema } from './util/schemas'; import type { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from '../types'; diff --git a/src/plugins/data_views/server/routes/has_data_views.test.ts b/src/plugins/data_views/server/routes/has_data_views.test.ts index 70967ce85edf3..d565d35ede302 100644 --- a/src/plugins/data_views/server/routes/has_data_views.test.ts +++ b/src/plugins/data_views/server/routes/has_data_views.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { CoreSetup, RequestHandlerContext } from '@kbn/core/server'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; import { registerHasDataViewsRoute } from './has_data_views'; diff --git a/src/plugins/data_views/server/saved_objects_client_wrapper.ts b/src/plugins/data_views/server/saved_objects_client_wrapper.ts index 85b7710614536..d8755b9ff1be1 100644 --- a/src/plugins/data_views/server/saved_objects_client_wrapper.ts +++ b/src/plugins/data_views/server/saved_objects_client_wrapper.ts @@ -7,11 +7,8 @@ */ import { SavedObjectsClientContract, SavedObject } from '@kbn/core/server'; -import { - SavedObjectsClientCommon, - SavedObjectsClientCommonFindArgs, - DataViewSavedObjectConflictError, -} from '../common'; +import { SavedObjectsClientCommon, SavedObjectsClientCommonFindArgs } from '../common/types'; +import { DataViewSavedObjectConflictError } from '../common/errors'; export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommon { private savedObjectClient: SavedObjectsClientContract; diff --git a/src/plugins/data_views/server/types.ts b/src/plugins/data_views/server/types.ts index 5e366b328275b..cce27ff305972 100644 --- a/src/plugins/data_views/server/types.ts +++ b/src/plugins/data_views/server/types.ts @@ -17,42 +17,74 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import { DataViewsService } from '../common'; +/** + * Data Views service factory + */ type ServiceFactory = ( + /** + * Saved objects client + */ savedObjectsClient: SavedObjectsClientContract, + /** + * Elasticsearch client + */ elasticsearchClient: ElasticsearchClient, + /** + * Kibana request object + */ request?: KibanaRequest, + /** + * Ignore capabilities + */ byPassCapabilities?: boolean ) => Promise; + +/** + * DataViews server plugin start api + */ export interface DataViewsServerPluginStart { - dataViewsServiceFactory: ServiceFactory; /** - * @deprecated Renamed to dataViewsServiceFactory + * Returns a DataViews service instance */ - indexPatternsServiceFactory: ServiceFactory; -} - -export interface IndexPatternsServiceSetupDeps { - expressions: ExpressionsServerSetup; - usageCollection?: UsageCollectionSetup; -} - -export interface IndexPatternsServiceStartDeps { - fieldFormats: FieldFormatsStart; - logger: Logger; + dataViewsServiceFactory: ServiceFactory; } +/** + * DataViews server plugin setup api + */ // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataViewsServerPluginSetup {} -export type IndexPatternsServiceStart = DataViewsServerPluginStart; - +/** + * Data Views server setup dependencies + * @public + */ export interface DataViewsServerPluginSetupDependencies { + /** + * File formats + */ fieldFormats: FieldFormatsSetup; + /** + * Expressions + */ expressions: ExpressionsServerSetup; + /** + * Usage collection + */ usageCollection?: UsageCollectionSetup; } +/** + * Data Views server start dependencies + * @public + */ export interface DataViewsServerPluginStartDependencies { + /** + * Field formats + */ fieldFormats: FieldFormatsStart; + /** + * Logger + */ logger: Logger; } diff --git a/src/plugins/data_views/server/utils.ts b/src/plugins/data_views/server/utils.ts index d5d6082dc5ab8..79374609cdaa0 100644 --- a/src/plugins/data_views/server/utils.ts +++ b/src/plugins/data_views/server/utils.ts @@ -9,6 +9,9 @@ import { SavedObjectsClientContract } from '@kbn/core/server'; import { DATA_VIEW_SAVED_OBJECT_TYPE, DataViewAttributes, SavedObject, FieldSpec } from '../common'; +/** + * @deprecated Use data views api instead + */ export const getFieldByName = ( fieldName: string, indexPattern: SavedObject @@ -19,6 +22,9 @@ export const getFieldByName = ( return field; }; +/** + * @deprecated Use data views api instead + */ export const findIndexPatternById = async ( savedObjectsClient: SavedObjectsClientContract, index: string diff --git a/src/plugins/discover/public/__mocks__/data_view_complex.ts b/src/plugins/discover/public/__mocks__/data_view_complex.ts new file mode 100644 index 0000000000000..ff26114ffcdaa --- /dev/null +++ b/src/plugins/discover/public/__mocks__/data_view_complex.ts @@ -0,0 +1,419 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { DataView } from '@kbn/data-views-plugin/public'; +import { buildDataViewMock } from './index_pattern'; + +const fields = [ + { + count: 0, + name: '_id', + type: 'string', + esTypes: ['_id'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_index', + type: 'string', + esTypes: ['_index'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: '_score', + type: 'number', + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_source', + type: '_source', + esTypes: ['_source'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: '_type', + type: 'string', + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 2, + name: 'array_objects.description', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'array_objects.description.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'array_objects.description', + }, + }, + }, + { + count: 0, + name: 'array_objects.name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'array_objects.name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'array_objects.name', + }, + }, + }, + { + count: 0, + name: 'array_tags', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'array_tags.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'array_tags', + }, + }, + }, + { + count: 0, + name: 'binary_blob', + type: 'unknown', + esTypes: ['binary'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'bool_enabled', + type: 'boolean', + esTypes: ['boolean'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'date', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 1, + name: 'date_nanos', + type: 'date', + esTypes: ['date_nanos'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'flattened_labels', + type: 'unknown', + esTypes: ['flattened'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geo_point', + type: 'geo_point', + esTypes: ['geo_point'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 1, + name: 'geometry', + type: 'unknown', + esTypes: ['shape'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 1, + name: 'histogram', + type: 'histogram', + esTypes: ['histogram'], + scripted: false, + searchable: false, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'ip_addr', + type: 'ip', + esTypes: ['ip'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 4, + name: 'keyword_key', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'nested_user.first', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { + nested: { + path: 'nested_user', + }, + }, + }, + { + count: 0, + name: 'nested_user.first.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'nested_user.first', + }, + nested: { + path: 'nested_user', + }, + }, + }, + { + count: 0, + name: 'nested_user.last', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { + nested: { + path: 'nested_user', + }, + }, + }, + { + count: 0, + name: 'nested_user.last.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'nested_user.last', + }, + nested: { + path: 'nested_user', + }, + }, + }, + { + count: 3, + name: 'number_amount', + type: 'number', + esTypes: ['long'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 3, + name: 'number_price', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'object_user.first', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'object_user.last', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'range_time_frame', + type: 'date_range', + esTypes: ['date_range'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 1, + name: 'rank_features', + type: 'unknown', + esTypes: ['rank_features'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 5, + name: 'text_message', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'vector', + type: 'unknown', + esTypes: ['dense_vector'], + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'version', + type: 'string', + esTypes: ['version'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 1, + script: 'return "hi there"', + lang: 'painless', + name: 'scripted_string', + type: 'string', + scripted: true, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, + { + count: 0, + name: 'runtime_number', + type: 'number', + esTypes: ['double'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }, +] as DataView['fields']; + +export const dataViewComplexMock = buildDataViewMock({ + name: 'data-view-with-various-field-types', + fields, + timeFieldName: 'data', +}); diff --git a/src/plugins/discover/public/__mocks__/es_hits_complex.ts b/src/plugins/discover/public/__mocks__/es_hits_complex.ts new file mode 100644 index 0000000000000..ecbc93a04cf22 --- /dev/null +++ b/src/plugins/discover/public/__mocks__/es_hits_complex.ts @@ -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 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. + */ + +export const esHitsComplex = [ + { + _index: 'sample', + _id: '1', + _version: 2, + _score: null, + fields: { + date: ['2022-05-22T12:10:30.000Z'], + 'array_objects.description.keyword': ['programming list', 'cool stuff list'], + rank_features: [ + { + '2star': 100, + '1star': 10, + }, + ], + array_tags: ['elasticsearch', 'wow'], + 'array_objects.name.keyword': ['prog_list', 'cool_list'], + flattened_labels: [ + { + release: ['v1.2.5', 'v1.3.0'], + priority: 'urgent', + }, + ], + geo_point: [ + { + coordinates: [-71.34, 41.12], + type: 'Point', + }, + ], + binary_blob: ['U29tZSBiaW5hcnkgYmxvYg=='], + text_message: ['Hi there! I am a sample string.'], + 'object_user.first': ['John'], + keyword_key: ['abcd1'], + 'array_objects.name': ['prog_list', 'cool_list'], + vector: [0.5, 10, 6], + nested_user: [ + { + last: ['Smith'], + 'last.keyword': ['Smith'], + first: ['John'], + 'first.keyword': ['John'], + }, + { + last: ['White'], + 'last.keyword': ['White'], + first: ['Alice'], + 'first.keyword': ['Alice'], + }, + ], + number_amount: [50], + 'array_tags.keyword': ['elasticsearch', 'wow'], + bool_enabled: [false], + version: ['1.2.3'], + histogram: [ + { + counts: [3, 7, 23, 12, 6], + values: [0.1, 0.2, 0.3, 0.4, 0.5], + }, + ], + 'array_objects.description': ['programming list', 'cool stuff list'], + range_time_frame: [ + { + gte: '2015-10-31 12:00:00', + lte: '2015-11-01 00:00:00', + }, + ], + number_price: [10.99], + 'object_user.last': ['Smith'], + geometry: [ + { + coordinates: [ + [ + [1000, -1001], + [1001, -1001], + [1001, -1000], + [1000, -1000], + [1000, -1001], + ], + ], + type: 'Polygon', + }, + ], + date_nanos: ['2022-01-01T12:10:30.123456789Z'], + ip_addr: ['192.168.1.1'], + runtime_number: [5.5], + scripted_string: ['hi there'], + }, + sort: [1653221430000], + }, + { + _index: 'sample', + _id: '2', + _version: 8, + _score: null, + fields: { + date: ['2022-05-20T00:00:00.000Z'], + 'array_objects.description.keyword': ['elastic list'], + rank_features: [ + { + '2star': 350, + '1star': 20, + }, + ], + array_tags: ['=1+2\'" ;,=1+2'], + 'array_objects.name.keyword': ['elastic_list'], + flattened_labels: [ + { + release: ['v1.4.5'], + priority: 'minor', + }, + ], + geo_point: [ + { + coordinates: [-71.34, 41.12], + type: 'Point', + }, + ], + binary_blob: ['U29tZSBiaW5hcnkgYmxvYg=='], + text_message: ["I'm multiline\n*&%$#@"], + 'object_user.first': ['Jane'], + keyword_key: ['=1+2";=1+2'], + 'array_objects.name': ['elastic_list'], + vector: [0.5, 12, 6], + nested_user: [ + { + last: ['Smith'], + 'last.keyword': ['Smith'], + first: ['Jane'], + 'first.keyword': ['Jane'], + }, + ], + number_amount: [10], + 'array_tags.keyword': ['=1+2\'" ;,=1+2'], + bool_enabled: [true], + version: ['1.3.3'], + histogram: [ + { + counts: [8, 17, 8, 7, 6, 2], + values: [0.1, 0.25, 0.35, 0.4, 0.45, 0.5], + }, + ], + 'array_objects.description': ['elastic list'], + range_time_frame: [ + { + gte: '2015-10-31 12:00:00', + lte: '2016-11-01 00:00:00', + }, + ], + number_price: [105.99], + 'object_user.last': ['Smith'], + geometry: [ + { + geometries: [ + { + coordinates: [1000, 100], + type: 'Point', + }, + { + coordinates: [ + [1001, 100], + [1002, 100], + ], + type: 'LineString', + }, + ], + type: 'GeometryCollection', + }, + ], + date_nanos: ['2022-01-02T11:10:30.123456789Z'], + ip_addr: ['192.168.1.0'], + runtime_number: [5.5], + scripted_string: ['=1+2";=1+2'], + }, + sort: [1653004800000], + }, +]; diff --git a/src/plugins/discover/public/__mocks__/grid_context.ts b/src/plugins/discover/public/__mocks__/grid_context.ts new file mode 100644 index 0000000000000..3f760bc4a4258 --- /dev/null +++ b/src/plugins/discover/public/__mocks__/grid_context.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { flattenHit } from '@kbn/data-plugin/common'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { indexPatternMock } from './index_pattern'; +import { dataViewComplexMock } from './data_view_complex'; +import { esHits } from './es_hits'; +import { esHitsComplex } from './es_hits_complex'; +import { discoverServiceMock } from './services'; +import { GridContext } from '../components/discover_grid/discover_grid_context'; +import { convertValueToString } from '../utils/convert_value_to_string'; +import type { ElasticSearchHit } from '../types'; + +const buildGridContext = (dataView: DataView, rows: ElasticSearchHit[]): GridContext => { + const rowsFlattened = rows.map((hit) => + flattenHit(hit, dataView, { includeIgnoredValues: true }) + ); + + return { + expanded: undefined, + setExpanded: jest.fn(), + rows, + rowsFlattened, + onFilter: jest.fn(), + indexPattern: dataView, + isDarkMode: false, + selectedDocs: [], + setSelectedDocs: jest.fn(), + valueToStringConverter: (rowIndex, columnId, options) => + convertValueToString({ + rowIndex, + columnId, + services: discoverServiceMock, + rows, + rowsFlattened, + dataView, + options, + }), + }; +}; + +export const discoverGridContextMock = buildGridContext(indexPatternMock, esHits); + +export const discoverGridContextComplexMock = buildGridContext(dataViewComplexMock, esHitsComplex); diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index 839acfcf3ea15..93b0727cfa41a 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -64,27 +64,42 @@ const fields = [ }, ] as DataView['fields']; -fields.getByName = (name: string) => { - return fields.find((field) => field.name === name); -}; +export const buildDataViewMock = ({ + name, + fields: definedFields, + timeFieldName, +}: { + name: string; + fields: DataView['fields']; + timeFieldName?: string; +}): DataView => { + const dataViewFields = [...definedFields] as DataView['fields']; -fields.getAll = () => { - return fields; -}; + dataViewFields.getByName = (fieldName: string) => { + return dataViewFields.find((field) => field.name === fieldName); + }; + + dataViewFields.getAll = () => { + return dataViewFields; + }; -const indexPattern = { - id: 'the-index-pattern-id', - title: 'the-index-pattern-title', - metaFields: ['_index', '_score'], - fields, - getComputedFields: () => ({ docvalueFields: [], scriptFields: {}, storedFields: ['*'] }), - getSourceFiltering: () => ({}), - getFieldByName: jest.fn(() => ({})), - timeFieldName: '', - docvalueFields: [], - getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), -} as unknown as DataView; + const dataView = { + id: `${name}-id`, + title: `${name}-title`, + metaFields: ['_index', '_score'], + fields: dataViewFields, + getComputedFields: () => ({ docvalueFields: [], scriptFields: {}, storedFields: ['*'] }), + getSourceFiltering: () => ({}), + getFieldByName: jest.fn((fieldName: string) => dataViewFields.getByName(fieldName)), + timeFieldName: timeFieldName || '', + docvalueFields: [], + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + isTimeNanosBased: () => false, + } as unknown as DataView; -indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; + dataView.isTimeBased = () => !!timeFieldName; + + return dataView; +}; -export const indexPatternMock = indexPattern; +export const indexPatternMock = buildDataViewMock({ name: 'the-index-pattern', fields }); diff --git a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts index 89a53e474b5b3..21e70bcd99e70 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts @@ -7,6 +7,7 @@ */ import { DataView } from '@kbn/data-views-plugin/public'; +import { buildDataViewMock } from './index_pattern'; const fields = [ { @@ -51,27 +52,8 @@ const fields = [ }, ] as DataView['fields']; -fields.getByName = (name: string) => { - return fields.find((field) => field.name === name); -}; -fields.getAll = () => { - return fields; -}; - -const indexPattern = { - id: 'index-pattern-with-timefield-id', - title: 'index-pattern-with-timefield', - metaFields: ['_index', '_score'], +export const indexPatternWithTimefieldMock = buildDataViewMock({ + name: 'index-pattern-with-timefield', fields, - getComputedFields: () => ({}), - getSourceFiltering: () => ({}), - getFieldByName: (name: string) => fields.getByName(name), timeFieldName: 'timestamp', - getFormatterForField: () => ({ convert: (value: unknown) => value }), - isTimeNanosBased: () => false, - popularizeField: () => {}, -} as unknown as DataView; - -indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; - -export const indexPatternWithTimefieldMock = indexPattern; +}); diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 81938c8a9ca06..f1865049947b7 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -95,4 +95,8 @@ export const discoverServiceMock = { }, storage: new LocalStorageMock({}) as unknown as Storage, addBasePath: jest.fn(), + toastNotifications: { + addInfo: jest.fn(), + addWarning: jest.fn(), + }, } as unknown as DiscoverServices; diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 71a0ef3df1b8c..48ba74c41cd2e 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -108,12 +108,7 @@ export function AlertsPopover({ name: 'Alerting', items: [ { - name: ( - <> - {SearchThresholdAlertFlyout} - {createSearchThresholdRuleLink} - - ), + name: <>{createSearchThresholdRuleLink}, icon: 'bell', disabled: !hasTimeFieldName, }, diff --git a/src/plugins/discover/public/components/discover_grid/build_copy_column_button.test.tsx b/src/plugins/discover/public/components/discover_grid/build_copy_column_button.test.tsx new file mode 100644 index 0000000000000..3d3dcab8a2f96 --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/build_copy_column_button.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 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 React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { discoverGridContextMock } from '../../__mocks__/grid_context'; +import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button'; + +const execCommandMock = (global.document.execCommand = jest.fn()); +const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + +describe('Build a column button to copy to clipboard', () => { + it('should copy a column name to clipboard on click', () => { + const { label, iconType, onClick } = buildCopyColumnNameButton({ + columnId: 'test-field-name', + services: discoverServiceMock, + }); + execCommandMock.mockImplementationOnce(() => true); + + const wrapper = mountWithIntl( + + {label} + + ); + + wrapper.find(EuiButton).simulate('click'); + + expect(execCommandMock).toHaveBeenCalledWith('copy'); + expect(warn).not.toHaveBeenCalled(); + }); + + it('should copy column values to clipboard on click', async () => { + const originalClipboard = global.window.navigator.clipboard; + + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: jest.fn(), + }, + writable: true, + }); + + const { label, iconType, onClick } = buildCopyColumnValuesButton({ + columnId: 'extension', + services: discoverServiceMock, + rowsCount: 3, + valueToStringConverter: discoverGridContextMock.valueToStringConverter, + }); + + const wrapper = mountWithIntl( + + {label} + + ); + + await wrapper.find(EuiButton).simulate('click'); + + // first row out of 3 rows does not have a value + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('extension\n\njpg\ngif'); + + const { + label: labelSource, + iconType: iconTypeSource, + onClick: onClickSource, + } = buildCopyColumnValuesButton({ + columnId: '_source', + services: discoverServiceMock, + valueToStringConverter: discoverGridContextMock.valueToStringConverter, + rowsCount: 3, + }); + + const wrapperSource = mountWithIntl( + + {labelSource} + + ); + + await wrapperSource.find(EuiButton).simulate('click'); + + // first row out of 3 rows does not have a value + expect(navigator.clipboard.writeText).toHaveBeenNthCalledWith( + 2, + '"_source"\n{"bytes":20,"date":"2020-20-01T12:12:12.123","message":"test1","_index":"i","_score":1}\n' + + '{"date":"2020-20-01T12:12:12.124","extension":"jpg","name":"test2","_index":"i","_score":1}\n' + + '{"bytes":50,"date":"2020-20-01T12:12:12.124","extension":"gif","name":"test3","_index":"i","_score":1}' + ); + + Object.defineProperty(navigator, 'clipboard', { + value: originalClipboard, + }); + }); + + it('should not copy to clipboard on click', () => { + const { label, iconType, onClick } = buildCopyColumnNameButton({ + columnId: 'test-field-name', + services: discoverServiceMock, + }); + execCommandMock.mockImplementationOnce(() => false); + + const wrapper = mountWithIntl( + + {label} + + ); + + wrapper.find(EuiButton).simulate('click'); + + expect(execCommandMock).toHaveBeenCalledWith('copy'); + expect(warn).toHaveBeenCalledWith('Unable to copy to clipboard.'); + }); +}); diff --git a/src/plugins/discover/public/components/discover_grid/build_copy_column_button.tsx b/src/plugins/discover/public/components/discover_grid/build_copy_column_button.tsx new file mode 100644 index 0000000000000..60ae5182965c1 --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/build_copy_column_button.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 React from 'react'; +import { EuiListGroupItemProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + copyColumnValuesToClipboard, + copyColumnNameToClipboard, +} from '../../utils/copy_value_to_clipboard'; +import { DiscoverServices } from '../../build_services'; +import type { ValueToStringConverter } from '../../types'; + +function buildCopyColumnButton({ + label, + onCopy, + dataTestSubj, +}: { + label: EuiListGroupItemProps['label']; + onCopy: () => unknown; + dataTestSubj: string; +}) { + const copyToClipBoardButton: EuiListGroupItemProps = { + size: 'xs', + label, + iconType: 'copyClipboard', + iconProps: { size: 'm' }, + onClick: onCopy, + 'data-test-subj': dataTestSubj, + }; + + return copyToClipBoardButton; +} + +export function buildCopyColumnNameButton({ + columnId, + services, +}: { + columnId: string; + services: DiscoverServices; +}): EuiListGroupItemProps { + return buildCopyColumnButton({ + label: ( + + ), + onCopy: () => copyColumnNameToClipboard({ columnId, services }), + dataTestSubj: 'gridCopyColumnNameToClipBoardButton', + }); +} + +export function buildCopyColumnValuesButton({ + columnId, + services, + rowsCount, + valueToStringConverter, +}: { + columnId: string; + services: DiscoverServices; + rowsCount: number; + valueToStringConverter: ValueToStringConverter; +}): EuiListGroupItemProps { + return buildCopyColumnButton({ + label: ( + + ), + onCopy: () => + copyColumnValuesToClipboard({ + columnId, + services, + rowsCount, + valueToStringConverter, + }), + dataTestSubj: 'gridCopyColumnValuesToClipBoardButton', + }); +} diff --git a/src/plugins/discover/public/components/discover_grid/copy_column_name_button.test.tsx b/src/plugins/discover/public/components/discover_grid/copy_column_name_button.test.tsx deleted file mode 100644 index 66bfefe32f6a0..0000000000000 --- a/src/plugins/discover/public/components/discover_grid/copy_column_name_button.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 React from 'react'; -import { buildCopyColumnNameButton } from './copy_column_name_button'; -import { EuiButton } from '@elastic/eui'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; - -const execCommandMock = (global.document.execCommand = jest.fn()); -const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); - -describe('Copy to clipboard button', () => { - it('should copy to clipboard on click', () => { - const { label, iconType, onClick } = buildCopyColumnNameButton('test-field-name'); - execCommandMock.mockImplementationOnce(() => true); - - const wrapper = mountWithIntl( - - {label} - - ); - - wrapper.find(EuiButton).simulate('click'); - - expect(execCommandMock).toHaveBeenCalledWith('copy'); - expect(warn).not.toHaveBeenCalled(); - }); - - it('should not copy to clipboard on click', () => { - const { label, iconType, onClick } = buildCopyColumnNameButton('test-field-name'); - execCommandMock.mockImplementationOnce(() => false); - - const wrapper = mountWithIntl( - - {label} - - ); - - wrapper.find(EuiButton).simulate('click'); - - expect(execCommandMock).toHaveBeenCalledWith('copy'); - expect(warn).toHaveBeenCalledWith('Unable to copy to clipboard.'); - }); -}); diff --git a/src/plugins/discover/public/components/discover_grid/copy_column_name_button.tsx b/src/plugins/discover/public/components/discover_grid/copy_column_name_button.tsx deleted file mode 100644 index e3f972ff3df26..0000000000000 --- a/src/plugins/discover/public/components/discover_grid/copy_column_name_button.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 React from 'react'; -import { copyToClipboard, EuiListGroupItemProps } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -export function buildCopyColumnNameButton(columnName: string) { - const copyToClipBoardButton: EuiListGroupItemProps = { - size: 'xs', - label: ( - - ), - iconType: 'copyClipboard', - iconProps: { size: 'm' }, - onClick: () => copyToClipboard(columnName), - }; - - return copyToClipBoardButton; -} diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx index 64cbab5c1511b..eafc150843ef7 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx @@ -48,9 +48,10 @@ import { import { DiscoverGridDocumentToolbarBtn, getDocId } from './discover_grid_document_selection'; import { SortPairArr } from '../doc_table/lib/get_sort'; import { getFieldsToShow } from '../../utils/get_fields_to_show'; -import { ElasticSearchHit } from '../../types'; +import type { ElasticSearchHit, ValueToStringConverter } from '../../types'; import { useRowHeightsOptions } from '../../utils/use_row_heights_options'; import { useDiscoverServices } from '../../utils/use_discover_services'; +import { convertValueToString } from '../../utils/convert_value_to_string'; interface SortObj { id: string; @@ -237,6 +238,21 @@ export const DiscoverGrid = ({ }); }, [displayedRows, indexPattern]); + const valueToStringConverter: ValueToStringConverter = useCallback( + (rowIndex, columnId, options) => { + return convertValueToString({ + rowIndex, + rows: displayedRows, + rowsFlattened: displayedRowsFlattened, + dataView: indexPattern, + columnId, + services, + options, + }); + }, + [displayedRows, displayedRowsFlattened, indexPattern, services] + ); + /** * Pagination */ @@ -319,15 +335,28 @@ export const DiscoverGrid = ({ const euiGridColumns = useMemo( () => - getEuiGridColumns( - displayedColumns, + getEuiGridColumns({ + columns: displayedColumns, + rowsCount: displayedRows.length, settings, indexPattern, showTimeCol, defaultColumns, - isSortEnabled - ), - [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns, isSortEnabled] + isSortEnabled, + services, + valueToStringConverter, + }), + [ + displayedColumns, + displayedRows, + indexPattern, + showTimeCol, + settings, + defaultColumns, + isSortEnabled, + services, + valueToStringConverter, + ] ); const hideTimeColumn = useMemo( @@ -452,6 +481,7 @@ export const DiscoverGrid = ({ setIsFilterActive(false); } }, + valueToStringConverter, }} > true); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { @@ -33,22 +33,8 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { FilterInBtn, FilterOutBtn, buildCellActions, CopyBtn } from './discover_grid_cell_actions'; import { DiscoverGridContext } from './discover_grid_context'; import { EuiButton } from '@elastic/eui'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { esHits } from '../../__mocks__/es_hits'; +import { discoverGridContextMock } from '../../__mocks__/grid_context'; import { DataViewField } from '@kbn/data-views-plugin/public'; -import { flattenHit } from '@kbn/data-plugin/common'; - -const contextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - rowsFlattened: esHits.map((hit) => flattenHit(hit, indexPatternMock)), - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), -}; describe('Discover cell actions ', function () { it('should not show cell actions for unfilterable fields', async () => { @@ -57,7 +43,7 @@ describe('Discover cell actions ', function () { it('triggers filter function when FilterInBtn is clicked', async () => { const component = mountWithIntl( - + } @@ -70,15 +56,15 @@ describe('Discover cell actions ', function () { ); const button = findTestSubject(component, 'filterForButton'); await button.simulate('click'); - expect(contextMock.onFilter).toHaveBeenCalledWith( - indexPatternMock.fields.getByName('extension'), + expect(discoverGridContextMock.onFilter).toHaveBeenCalledWith( + discoverGridContextMock.indexPattern.fields.getByName('extension'), 'jpg', '+' ); }); it('triggers filter function when FilterOutBtn is clicked', async () => { const component = mountWithIntl( - + } @@ -91,15 +77,15 @@ describe('Discover cell actions ', function () { ); const button = findTestSubject(component, 'filterOutButton'); await button.simulate('click'); - expect(contextMock.onFilter).toHaveBeenCalledWith( - indexPatternMock.fields.getByName('extension'), + expect(discoverGridContextMock.onFilter).toHaveBeenCalledWith( + discoverGridContextMock.indexPattern.fields.getByName('extension'), 'jpg', '-' ); }); it('triggers clipboard copy when CopyBtn is clicked', async () => { const component = mountWithIntl( - + } diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx index df07478dae5c6..8065474e49cb5 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx @@ -7,12 +7,12 @@ */ import React, { useContext } from 'react'; -import { copyToClipboard, EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataViewField } from '@kbn/data-views-plugin/public'; import { DiscoverGridContext, GridContext } from './discover_grid_context'; import { useDiscoverServices } from '../../utils/use_discover_services'; -import { formatFieldValue } from '../../utils/format_value'; +import { copyValueToClipboard } from '../../utils/copy_value_to_clipboard'; function onFilterCell( context: GridContext, @@ -86,8 +86,8 @@ export const FilterOutBtn = ({ }; export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) => { - const { indexPattern: dataView, rowsFlattened, rows } = useContext(DiscoverGridContext); - const { fieldFormats, toastNotifications } = useDiscoverServices(); + const { valueToStringConverter } = useContext(DiscoverGridContext); + const services = useDiscoverServices(); const buttonTitle = i18n.translate('discover.grid.copyClipboardButtonTitle', { defaultMessage: 'Copy value of {column}', @@ -97,22 +97,11 @@ export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCell return ( { - const rowFlattened = rowsFlattened[rowIndex]; - const field = dataView.fields.getByName(columnId); - const value = rowFlattened[columnId]; - - const valueFormatted = - field?.type === '_source' - ? JSON.stringify(rowFlattened, null, 2) - : formatFieldValue(value, rows[rowIndex], fieldFormats, dataView, field, 'text'); - copyToClipboard(valueFormatted); - const infoTitle = i18n.translate('discover.grid.copyClipboardToastTitle', { - defaultMessage: 'Copied value of {column} to clipboard.', - values: { column: columnId }, - }); - - toastNotifications.addInfo({ - title: infoTitle, + copyValueToClipboard({ + rowIndex, + columnId, + services, + valueToStringConverter, }); }} iconType="copyClipboard" diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx index c98db31a97f7f..bdefa126f07d8 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx @@ -9,30 +9,50 @@ import { indexPatternMock } from '../../__mocks__/index_pattern'; import { getEuiGridColumns } from './discover_grid_columns'; import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; +import { discoverGridContextMock } from '../../__mocks__/grid_context'; +import { discoverServiceMock } from '../../__mocks__/services'; describe('Discover grid columns', function () { it('returns eui grid columns without time column', async () => { - const actual = getEuiGridColumns( - ['extension', 'message'], - {}, - indexPatternMock, - false, - false, - true - ); + const actual = getEuiGridColumns({ + columns: ['extension', 'message'], + settings: {}, + indexPattern: indexPatternMock, + showTimeCol: false, + defaultColumns: false, + isSortEnabled: true, + valueToStringConverter: discoverGridContextMock.valueToStringConverter, + rowsCount: 100, + services: discoverServiceMock, + }); expect(actual).toMatchInlineSnapshot(` Array [ Object { "actions": Object { "additional": Array [ Object { + "data-test-subj": "gridCopyColumnNameToClipBoardButton", "iconProps": Object { "size": "m", }, "iconType": "copyClipboard", "label": , + "onClick": [Function], + "size": "xs", + }, + Object { + "data-test-subj": "gridCopyColumnValuesToClipBoardButton", + "iconProps": Object { + "size": "m", + }, + "iconType": "copyClipboard", + "label": , "onClick": [Function], @@ -46,23 +66,41 @@ describe('Discover grid columns', function () { "showMoveLeft": true, "showMoveRight": true, }, - "cellActions": undefined, - "display": undefined, + "cellActions": Array [ + [Function], + [Function], + ], + "display": "extension", "id": "extension", "isSortable": false, - "schema": "kibana-json", + "schema": "string", }, Object { "actions": Object { "additional": Array [ Object { + "data-test-subj": "gridCopyColumnNameToClipBoardButton", "iconProps": Object { "size": "m", }, "iconType": "copyClipboard", "label": , + "onClick": [Function], + "size": "xs", + }, + Object { + "data-test-subj": "gridCopyColumnValuesToClipBoardButton", + "iconProps": Object { + "size": "m", + }, + "iconType": "copyClipboard", + "label": , "onClick": [Function], @@ -77,36 +115,54 @@ describe('Discover grid columns', function () { "showMoveRight": true, }, "cellActions": undefined, - "display": undefined, + "display": "message", "id": "message", "isSortable": false, - "schema": "kibana-json", + "schema": "string", }, ] `); }); it('returns eui grid columns without time column showing default columns', async () => { - const actual = getEuiGridColumns( - ['extension', 'message'], - {}, - indexPatternWithTimefieldMock, - false, - true, - true - ); + const actual = getEuiGridColumns({ + columns: ['extension', 'message'], + settings: {}, + indexPattern: indexPatternWithTimefieldMock, + showTimeCol: false, + defaultColumns: true, + isSortEnabled: true, + valueToStringConverter: discoverGridContextMock.valueToStringConverter, + rowsCount: 100, + services: discoverServiceMock, + }); expect(actual).toMatchInlineSnapshot(` Array [ Object { "actions": Object { "additional": Array [ Object { + "data-test-subj": "gridCopyColumnNameToClipBoardButton", "iconProps": Object { "size": "m", }, "iconType": "copyClipboard", "label": , + "onClick": [Function], + "size": "xs", + }, + Object { + "data-test-subj": "gridCopyColumnValuesToClipBoardButton", + "iconProps": Object { + "size": "m", + }, + "iconType": "copyClipboard", + "label": , "onClick": [Function], @@ -130,13 +186,28 @@ describe('Discover grid columns', function () { "actions": Object { "additional": Array [ Object { + "data-test-subj": "gridCopyColumnNameToClipBoardButton", + "iconProps": Object { + "size": "m", + }, + "iconType": "copyClipboard", + "label": , + "onClick": [Function], + "size": "xs", + }, + Object { + "data-test-subj": "gridCopyColumnValuesToClipBoardButton", "iconProps": Object { "size": "m", }, "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -157,27 +228,45 @@ describe('Discover grid columns', function () { `); }); it('returns eui grid columns with time column', async () => { - const actual = getEuiGridColumns( - ['extension', 'message'], - {}, - indexPatternWithTimefieldMock, - true, - false, - true - ); + const actual = getEuiGridColumns({ + columns: ['extension', 'message'], + settings: {}, + indexPattern: indexPatternWithTimefieldMock, + showTimeCol: true, + defaultColumns: false, + isSortEnabled: true, + valueToStringConverter: discoverGridContextMock.valueToStringConverter, + rowsCount: 100, + services: discoverServiceMock, + }); expect(actual).toMatchInlineSnapshot(` Array [ Object { "actions": Object { "additional": Array [ Object { + "data-test-subj": "gridCopyColumnNameToClipBoardButton", + "iconProps": Object { + "size": "m", + }, + "iconType": "copyClipboard", + "label": , + "onClick": [Function], + "size": "xs", + }, + Object { + "data-test-subj": "gridCopyColumnValuesToClipBoardButton", "iconProps": Object { "size": "m", }, "iconType": "copyClipboard", "label": , "onClick": [Function], @@ -215,13 +304,28 @@ describe('Discover grid columns', function () { "actions": Object { "additional": Array [ Object { + "data-test-subj": "gridCopyColumnNameToClipBoardButton", "iconProps": Object { "size": "m", }, "iconType": "copyClipboard", "label": , + "onClick": [Function], + "size": "xs", + }, + Object { + "data-test-subj": "gridCopyColumnValuesToClipBoardButton", + "iconProps": Object { + "size": "m", + }, + "iconType": "copyClipboard", + "label": , "onClick": [Function], @@ -248,13 +352,28 @@ describe('Discover grid columns', function () { "actions": Object { "additional": Array [ Object { + "data-test-subj": "gridCopyColumnNameToClipBoardButton", + "iconProps": Object { + "size": "m", + }, + "iconType": "copyClipboard", + "label": , + "onClick": [Function], + "size": "xs", + }, + Object { + "data-test-subj": "gridCopyColumnValuesToClipBoardButton", "iconProps": Object { "size": "m", }, "iconType": "copyClipboard", "label": , "onClick": [Function], diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx index d441c3e5aba75..1d4feefaec992 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx @@ -12,11 +12,13 @@ import { EuiDataGridColumn, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui import type { DataView } from '@kbn/data-views-plugin/public'; import { ExpandButton } from './discover_grid_expand_button'; import { DiscoverGridSettings } from './types'; +import type { ValueToStringConverter } from '../../types'; import { buildCellActions } from './discover_grid_cell_actions'; import { getSchemaByKbnType } from './discover_grid_schema'; import { SelectButton } from './discover_grid_document_selection'; import { defaultTimeColumnWidth } from './constants'; -import { buildCopyColumnNameButton } from './copy_column_name_button'; +import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button'; +import { DiscoverServices } from '../../build_services'; export function getLeadControlColumns() { return [ @@ -51,13 +53,25 @@ export function getLeadControlColumns() { ]; } -export function buildEuiGridColumn( - columnName: string, - columnWidth: number | undefined = 0, - indexPattern: DataView, - defaultColumns: boolean, - isSortEnabled: boolean -) { +export function buildEuiGridColumn({ + columnName, + columnWidth = 0, + indexPattern, + defaultColumns, + isSortEnabled, + services, + valueToStringConverter, + rowsCount, +}: { + columnName: string; + columnWidth: number | undefined; + indexPattern: DataView; + defaultColumns: boolean; + isSortEnabled: boolean; + services: DiscoverServices; + valueToStringConverter: ValueToStringConverter; + rowsCount: number; +}) { const indexPatternField = indexPattern.getFieldByName(columnName); const column: EuiDataGridColumn = { id: columnName, @@ -81,7 +95,17 @@ export function buildEuiGridColumn( }, showMoveLeft: !defaultColumns, showMoveRight: !defaultColumns, - additional: columnName === '_source' ? undefined : [buildCopyColumnNameButton(columnName)], + additional: [ + ...(columnName === '__source' + ? [] + : [buildCopyColumnNameButton({ columnId: columnName, services })]), + buildCopyColumnValuesButton({ + columnId: columnName, + services, + rowsCount, + valueToStringConverter, + }), + ], }, cellActions: indexPatternField ? buildCellActions(indexPatternField) : [], }; @@ -117,26 +141,46 @@ export function buildEuiGridColumn( return column; } -export function getEuiGridColumns( - columns: string[], - settings: DiscoverGridSettings | undefined, - indexPattern: DataView, - showTimeCol: boolean, - defaultColumns: boolean, - isSortEnabled: boolean -) { +export function getEuiGridColumns({ + columns, + rowsCount, + settings, + indexPattern, + showTimeCol, + defaultColumns, + isSortEnabled, + services, + valueToStringConverter, +}: { + columns: string[]; + rowsCount: number; + settings: DiscoverGridSettings | undefined; + indexPattern: DataView; + showTimeCol: boolean; + defaultColumns: boolean; + isSortEnabled: boolean; + services: DiscoverServices; + valueToStringConverter: ValueToStringConverter; +}) { const timeFieldName = indexPattern.timeFieldName; const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; + let visibleColumns = columns; if (showTimeCol && indexPattern.timeFieldName && !columns.find((col) => col === timeFieldName)) { - const usedColumns = [indexPattern.timeFieldName, ...columns]; - return usedColumns.map((column) => - buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled) - ); + visibleColumns = [indexPattern.timeFieldName, ...columns]; } - return columns.map((column) => - buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled) + return visibleColumns.map((column) => + buildEuiGridColumn({ + columnName: column, + columnWidth: getColWidth(column), + indexPattern, + defaultColumns, + isSortEnabled, + services, + valueToStringConverter, + rowsCount, + }) ); } diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx index f1b21dabab86e..ca2a0ee5839cb 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx @@ -9,18 +9,19 @@ import React from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; -import type { ElasticSearchHit } from '../../types'; +import type { ElasticSearchHit, HitsFlattened, ValueToStringConverter } from '../../types'; export interface GridContext { expanded?: ElasticSearchHit; setExpanded: (hit?: ElasticSearchHit) => void; rows: ElasticSearchHit[]; - rowsFlattened: Array>; + rowsFlattened: HitsFlattened; onFilter: DocViewFilterFn; indexPattern: DataView; isDarkMode: boolean; selectedDocs: string[]; setSelectedDocs: (selected: string[]) => void; + valueToStringConverter: ValueToStringConverter; } const defaultContext = {} as unknown as GridContext; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx index f1d8ab9fcb86d..5ca7b84789e91 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx @@ -13,22 +13,9 @@ import { getDocId, SelectButton, } from './discover_grid_document_selection'; -import { esHits } from '../../__mocks__/es_hits'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { discoverGridContextMock } from '../../__mocks__/grid_context'; import { DiscoverGridContext } from './discover_grid_context'; -const baseContextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - rowsFlattened: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), -}; - describe('document selection', () => { describe('getDocId', () => { test('doc with custom routing', () => { @@ -51,7 +38,7 @@ describe('document selection', () => { describe('SelectButton', () => { test('is not checked', () => { const contextMock = { - ...baseContextMock, + ...discoverGridContextMock, }; const component = mountWithIntl( @@ -74,7 +61,7 @@ describe('document selection', () => { test('is checked', () => { const contextMock = { - ...baseContextMock, + ...discoverGridContextMock, selectedDocs: ['i::1::'], }; @@ -98,7 +85,7 @@ describe('document selection', () => { test('adding a selection', () => { const contextMock = { - ...baseContextMock, + ...discoverGridContextMock, }; const component = mountWithIntl( @@ -121,7 +108,7 @@ describe('document selection', () => { }); test('removing a selection', () => { const contextMock = { - ...baseContextMock, + ...discoverGridContextMock, selectedDocs: ['i::1::'], }; @@ -148,7 +135,7 @@ describe('document selection', () => { test('it renders a button clickable button', () => { const props = { isFilterActive: false, - rows: esHits, + rows: discoverGridContextMock.rows, selectedDocs: ['i::1::'], setIsFilterActive: jest.fn(), setSelectedDocs: jest.fn(), diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx index 903d0bc4bedcd..e95853b99b7b7 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx @@ -11,25 +11,12 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ExpandButton } from './discover_grid_expand_button'; import { DiscoverGridContext } from './discover_grid_context'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { esHits } from '../../__mocks__/es_hits'; - -const baseContextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - rowsFlattened: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), -}; +import { discoverGridContextMock } from '../../__mocks__/grid_context'; describe('Discover grid view button ', function () { it('when no document is expanded, setExpanded is called with current document', async () => { const contextMock = { - ...baseContextMock, + ...discoverGridContextMock, }; const component = mountWithIntl( @@ -47,12 +34,12 @@ describe('Discover grid view button ', function () { ); const button = findTestSubject(component, 'docTableExpandToggleColumn'); await button.simulate('click'); - expect(contextMock.setExpanded).toHaveBeenCalledWith(esHits[0]); + expect(contextMock.setExpanded).toHaveBeenCalledWith(discoverGridContextMock.rows[0]); }); it('when the current document is expanded, setExpanded is called with undefined', async () => { const contextMock = { - ...baseContextMock, - expanded: esHits[0], + ...discoverGridContextMock, + expanded: discoverGridContextMock.rows[0], }; const component = mountWithIntl( @@ -74,8 +61,8 @@ describe('Discover grid view button ', function () { }); it('when another document is expanded, setExpanded is called with the current document', async () => { const contextMock = { - ...baseContextMock, - expanded: esHits[0], + ...discoverGridContextMock, + expanded: discoverGridContextMock.rows[0], }; const component = mountWithIntl( @@ -93,6 +80,6 @@ describe('Discover grid view button ', function () { ); const button = findTestSubject(component, 'docTableExpandToggleColumn'); await button.simulate('click'); - expect(contextMock.setExpanded).toHaveBeenCalledWith(esHits[1]); + expect(contextMock.setExpanded).toHaveBeenCalledWith(discoverGridContextMock.rows[1]); }); }); diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx index 4175ff1bdd7b5..8b6387a3574db 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx @@ -27,7 +27,7 @@ import { defaultMonacoEditorWidth } from './constants'; import { EsHitRecord } from '../../application/types'; import { formatFieldValue } from '../../utils/format_value'; import { formatHit } from '../../utils/format_hit'; -import { ElasticSearchHit } from '../../types'; +import { ElasticSearchHit, HitsFlattened } from '../../types'; import { useDiscoverServices } from '../../utils/use_discover_services'; import { MAX_DOC_FIELDS_DISPLAYED } from '../../../common'; @@ -37,7 +37,7 @@ export const getRenderCellValueFn = ( dataView: DataView, rows: ElasticSearchHit[] | undefined, - rowsFlattened: Array>, + rowsFlattened: HitsFlattened, useNewFieldsApi: boolean, fieldsToShow: string[], maxDocFieldsDisplayed: number, @@ -257,7 +257,6 @@ function getTopLevelObjectPairs( formatter.convert(val, 'html', { field: subField, hit: row, - indexPattern: dataView, }) ) .join(', '); diff --git a/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx b/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx index 932a952cec09f..50c1023afa211 100644 --- a/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx +++ b/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx @@ -72,7 +72,6 @@ export const formatTopLevelObject = ( formatter.convert(val, 'html', { field, hit: row, - indexPattern, }) ) .join(', '); diff --git a/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts index 9f9fc86b0288e..3e103d6ea3699 100644 --- a/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts +++ b/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts @@ -127,6 +127,7 @@ describe('getSavedSearch', () => { "setFields": [MockFunction], "setOverwriteDataViewType": [MockFunction], "setParent": [MockFunction], + "toExpressionAst": [MockFunction], }, "sharingSavedObjectProps": Object { "aliasPurpose": undefined, diff --git a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts index 9b42d7557b05c..f0958737d3b79 100644 --- a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts @@ -44,6 +44,9 @@ describe('saved_searches_utils', () => { "rowHeight": undefined, "searchSource": SearchSource { "dependencies": Object { + "aggs": Object { + "createAggConfigs": [MockFunction], + }, "getConfig": [MockFunction], "onResponse": [MockFunction], "search": [MockFunction], diff --git a/src/plugins/discover/public/types.ts b/src/plugins/discover/public/types.ts index f6872e9951c37..fa8582337349d 100644 --- a/src/plugins/discover/public/types.ts +++ b/src/plugins/discover/public/types.ts @@ -9,3 +9,11 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; export type ElasticSearchHit = estypes.SearchHit; + +export type HitsFlattened = Array>; + +export type ValueToStringConverter = ( + rowIndex: number, + columnId: string, + options?: { disableMultiline?: boolean } +) => { formattedString: string; withFormula: boolean }; diff --git a/src/plugins/discover/public/utils/convert_value_to_string.test.tsx b/src/plugins/discover/public/utils/convert_value_to_string.test.tsx new file mode 100644 index 0000000000000..e77d664851a23 --- /dev/null +++ b/src/plugins/discover/public/utils/convert_value_to_string.test.tsx @@ -0,0 +1,508 @@ +/* + * 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 { discoverGridContextComplexMock, discoverGridContextMock } from '../__mocks__/grid_context'; +import { discoverServiceMock } from '../__mocks__/services'; +import { convertValueToString, convertNameToString } from './convert_value_to_string'; + +describe('convertValueToString', () => { + it('should convert a keyword value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'keyword_key', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('abcd1'); + }); + + it('should convert a text value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'text_message', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"Hi there! I am a sample string."'); + }); + + it('should convert a multiline text value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'text_message', + rowIndex: 1, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"I\'m multiline\n*&%$#@"'); + expect(result.withFormula).toBe(false); + }); + + it('should convert a number value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'number_price', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('10.99'); + }); + + it('should convert a date value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'date', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"2022-05-22T12:10:30.000Z"'); + }); + + it('should convert a date nanos value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'date_nanos', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"2022-01-01T12:10:30.123456789Z"'); + }); + + it('should convert a boolean value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'bool_enabled', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('false'); + }); + + it('should convert a binary value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'binary_blob', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"U29tZSBiaW5hcnkgYmxvYg=="'); + }); + + it('should convert an object value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'object_user.first', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('John'); + }); + + it('should convert a nested value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'nested_user', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe( + '{"last":["Smith"],"last.keyword":["Smith"],"first":["John"],"first.keyword":["John"]}, {"last":["White"],"last.keyword":["White"],"first":["Alice"],"first.keyword":["Alice"]}' + ); + }); + + it('should convert a flattened value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'flattened_labels', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('{"release":["v1.2.5","v1.3.0"],"priority":"urgent"}'); + }); + + it('should convert a range value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'range_time_frame', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe( + '{"gte":"2015-10-31 12:00:00","lte":"2015-11-01 00:00:00"}' + ); + }); + + it('should convert a rank features value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'rank_features', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('{"2star":100,"1star":10}'); + }); + + it('should convert a histogram value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'histogram', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('{"counts":[3,7,23,12,6],"values":[0.1,0.2,0.3,0.4,0.5]}'); + }); + + it('should convert a IP value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'ip_addr', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"192.168.1.1"'); + }); + + it('should convert a version value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'version', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"1.2.3"'); + }); + + it('should convert a vector value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'vector', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('0.5, 10, 6'); + }); + + it('should convert a geo point value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'geo_point', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('{"coordinates":[-71.34,41.12],"type":"Point"}'); + }); + + it('should convert a geo point object value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'geo_point', + rowIndex: 1, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('{"coordinates":[-71.34,41.12],"type":"Point"}'); + }); + + it('should convert an array value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'array_tags', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('elasticsearch, wow'); + }); + + it('should convert a shape value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'geometry', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe( + '{"coordinates":[[[1000,-1001],[1001,-1001],[1001,-1000],[1000,-1000],[1000,-1001]]],"type":"Polygon"}' + ); + }); + + it('should convert a runtime value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'runtime_number', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('5.5'); + }); + + it('should convert a scripted value to text', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'scripted_string', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"hi there"'); + }); + + it('should return an empty string and not fail', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'unknown', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe(''); + }); + + it('should return an empty string when rowIndex is out of range', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'unknown', + rowIndex: -1, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe(''); + }); + + it('should return _source value', () => { + const result = convertValueToString({ + rows: discoverGridContextMock.rows, + rowsFlattened: discoverGridContextMock.rowsFlattened, + dataView: discoverGridContextMock.indexPattern, + services: discoverServiceMock, + columnId: '_source', + rowIndex: 0, + options: { + disableMultiline: false, + }, + }); + + expect(result.formattedString).toBe( + '{\n' + + ' "bytes": 20,\n' + + ' "date": "2020-20-01T12:12:12.123",\n' + + ' "message": "test1",\n' + + ' "_index": "i",\n' + + ' "_score": 1\n' + + '}' + ); + }); + + it('should return a formatted _source value', () => { + const result = convertValueToString({ + rows: discoverGridContextMock.rows, + rowsFlattened: discoverGridContextMock.rowsFlattened, + dataView: discoverGridContextMock.indexPattern, + services: discoverServiceMock, + columnId: '_source', + rowIndex: 0, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe( + '{"bytes":20,"date":"2020-20-01T12:12:12.123","message":"test1","_index":"i","_score":1}' + ); + }); + + it('should escape formula', () => { + const result = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'array_tags', + rowIndex: 1, + options: { + disableMultiline: true, + }, + }); + + expect(result.formattedString).toBe('"\'=1+2\'"" ;,=1+2"'); + expect(result.withFormula).toBe(true); + + const result2 = convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + columnId: 'scripted_string', + rowIndex: 1, + options: { + disableMultiline: true, + }, + }); + + expect(result2.formattedString).toBe('"\'=1+2"";=1+2"'); + expect(result2.withFormula).toBe(true); + }); + + it('should return a formatted name', () => { + const result = convertNameToString('test'); + + expect(result.formattedString).toBe('test'); + expect(result.withFormula).toBe(false); + }); + + it('should return a formatted name when with a formula', () => { + const result = convertNameToString('=1+2";=1+2'); + + expect(result.formattedString).toBe('"\'=1+2"";=1+2"'); + expect(result.withFormula).toBe(true); + }); +}); diff --git a/src/plugins/discover/public/utils/convert_value_to_string.ts b/src/plugins/discover/public/utils/convert_value_to_string.ts new file mode 100644 index 0000000000000..0f00725a69fb2 --- /dev/null +++ b/src/plugins/discover/public/utils/convert_value_to_string.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { DataView } from '@kbn/data-views-plugin/public'; +import { cellHasFormulas, createEscapeValue } from '@kbn/data-plugin/common'; +import { formatFieldValue } from './format_value'; +import { ElasticSearchHit, HitsFlattened } from '../types'; +import { DiscoverServices } from '../build_services'; + +interface ConvertedResult { + formattedString: string; + withFormula: boolean; +} + +export const convertValueToString = ({ + rowIndex, + rows, + rowsFlattened, + columnId, + dataView, + services, + options, +}: { + rowIndex: number; + rows: ElasticSearchHit[]; + rowsFlattened: HitsFlattened; + columnId: string; + dataView: DataView; + services: DiscoverServices; + options?: { + disableMultiline?: boolean; + }; +}): ConvertedResult => { + const { fieldFormats } = services; + const rowFlattened = rowsFlattened[rowIndex]; + const value = rowFlattened?.[columnId]; + const field = dataView.fields.getByName(columnId); + const valuesArray = Array.isArray(value) ? value : [value]; + const disableMultiline = options?.disableMultiline ?? false; + + if (field?.type === '_source') { + return { + formattedString: stringify(rowFlattened, disableMultiline), + withFormula: false, + }; + } + + let withFormula = false; + + const formatted = valuesArray + .map((subValue) => { + const formattedValue = formatFieldValue( + subValue, + rows[rowIndex], + fieldFormats, + dataView, + field, + 'text', + { + skipFormattingInStringifiedJSON: disableMultiline, + } + ); + + if (typeof formattedValue === 'string') { + withFormula = withFormula || cellHasFormulas(formattedValue); + return escapeFormattedValue(formattedValue); + } + + return stringify(formattedValue, disableMultiline) || ''; + }) + .join(', '); + + return { + formattedString: formatted, + withFormula, + }; +}; + +export const convertNameToString = (name: string): ConvertedResult => { + return { + formattedString: escapeFormattedValue(name), + withFormula: cellHasFormulas(name), + }; +}; + +const stringify = (val: object | string, disableMultiline: boolean) => { + // it can wrap "strings" with quotes + return disableMultiline ? JSON.stringify(val) : JSON.stringify(val, null, 2); +}; + +const escapeValueFn = createEscapeValue(true, true); + +const escapeFormattedValue = (formattedValue: string): string => { + return escapeValueFn(formattedValue); +}; diff --git a/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx b/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx new file mode 100644 index 0000000000000..004332f9a20cc --- /dev/null +++ b/src/plugins/discover/public/utils/copy_value_to_clipboard.test.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { discoverGridContextComplexMock } from '../__mocks__/grid_context'; +import { discoverServiceMock } from '../__mocks__/services'; +import { + copyValueToClipboard, + copyColumnNameToClipboard, + copyColumnValuesToClipboard, +} from './copy_value_to_clipboard'; +import { convertValueToString } from './convert_value_to_string'; +import type { ValueToStringConverter } from '../types'; + +const execCommandMock = (global.document.execCommand = jest.fn()); +const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + +describe('copyValueToClipboard', () => { + const valueToStringConverter: ValueToStringConverter = (rowIndex, columnId, options) => + convertValueToString({ + rows: discoverGridContextComplexMock.rows, + rowsFlattened: discoverGridContextComplexMock.rowsFlattened, + dataView: discoverGridContextComplexMock.indexPattern, + services: discoverServiceMock, + rowIndex, + columnId, + options, + }); + + const originalClipboard = global.window.navigator.clipboard; + + beforeAll(() => { + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: jest.fn(), + }, + writable: true, + }); + }); + + afterAll(() => { + Object.defineProperty(navigator, 'clipboard', { + value: originalClipboard, + }); + }); + + it('should copy a value to clipboard', () => { + execCommandMock.mockImplementationOnce(() => true); + const result = copyValueToClipboard({ + services: discoverServiceMock, + columnId: 'keyword_key', + rowIndex: 0, + valueToStringConverter, + }); + + expect(result).toBe('abcd1'); + expect(execCommandMock).toHaveBeenCalledWith('copy'); + expect(warn).not.toHaveBeenCalled(); + expect(discoverServiceMock.toastNotifications.addInfo).toHaveBeenCalledWith({ + title: 'Copied to clipboard', + }); + }); + + it('should inform when copy a value to clipboard failed', () => { + execCommandMock.mockImplementationOnce(() => false); + + const result = copyValueToClipboard({ + services: discoverServiceMock, + columnId: 'keyword_key', + rowIndex: 0, + valueToStringConverter, + }); + + expect(result).toBe(null); + expect(execCommandMock).toHaveBeenCalledWith('copy'); + expect(warn).toHaveBeenCalledWith('Unable to copy to clipboard.'); + expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith({ + title: 'Unable to copy to clipboard in this browser', + }); + }); + + it('should copy a column name to clipboard', () => { + execCommandMock.mockImplementationOnce(() => true); + const result = copyColumnNameToClipboard({ + services: discoverServiceMock, + columnId: 'text_message', + }); + + expect(result).toBe('"text_message"'); + expect(execCommandMock).toHaveBeenCalledWith('copy'); + expect(discoverServiceMock.toastNotifications.addInfo).toHaveBeenCalledWith({ + title: 'Copied to clipboard', + }); + }); + + it('should inform when copy a column name to clipboard failed', () => { + execCommandMock.mockImplementationOnce(() => false); + const result = copyColumnNameToClipboard({ + services: discoverServiceMock, + columnId: 'text_message', + }); + + expect(result).toBe(null); + expect(execCommandMock).toHaveBeenCalledWith('copy'); + expect(warn).toHaveBeenCalledWith('Unable to copy to clipboard.'); + expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith({ + title: 'Unable to copy to clipboard in this browser', + }); + }); + + it('should copy column values to clipboard', async () => { + execCommandMock.mockImplementationOnce(() => true); + + const result = await copyColumnValuesToClipboard({ + services: discoverServiceMock, + columnId: 'bool_enabled', + rowsCount: 2, + valueToStringConverter, + }); + + expect(result).toBe('"bool_enabled"\nfalse\ntrue'); + expect(global.window.navigator.clipboard.writeText).toHaveBeenCalledWith( + '"bool_enabled"\nfalse\ntrue' + ); + expect(discoverServiceMock.toastNotifications.addInfo).toHaveBeenCalledWith({ + title: 'Copied values of "bool_enabled" column to clipboard', + }); + }); + + it('should copy column values to clipboard with a warning', async () => { + execCommandMock.mockImplementationOnce(() => true); + const result = await copyColumnValuesToClipboard({ + services: discoverServiceMock, + columnId: 'scripted_string', + rowsCount: 2, + valueToStringConverter, + }); + + expect(result).toBe('"scripted_string"\n"hi there"\n"\'=1+2"";=1+2"'); + expect(discoverServiceMock.toastNotifications.addWarning).toHaveBeenCalledWith({ + title: 'Copied values of "scripted_string" column to clipboard', + text: 'It may contain formulas whose values have been escaped.', + }); + }); +}); diff --git a/src/plugins/discover/public/utils/copy_value_to_clipboard.ts b/src/plugins/discover/public/utils/copy_value_to_clipboard.ts new file mode 100644 index 0000000000000..eedaa676a4c56 --- /dev/null +++ b/src/plugins/discover/public/utils/copy_value_to_clipboard.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { copyToClipboard } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { ValueToStringConverter } from '../types'; +import { DiscoverServices } from '../build_services'; +import { convertNameToString } from './convert_value_to_string'; + +const WARNING_FOR_FORMULAS = i18n.translate( + 'discover.grid.copyEscapedValueWithFormulasToClipboardWarningText', + { + defaultMessage: 'It may contain formulas whose values have been escaped.', + } +); +const COPY_FAILED_ERROR_MESSAGE = i18n.translate('discover.grid.copyFailedErrorText', { + defaultMessage: 'Unable to copy to clipboard in this browser', +}); + +export const copyValueToClipboard = ({ + rowIndex, + columnId, + services, + valueToStringConverter, +}: { + rowIndex: number; + columnId: string; + services: DiscoverServices; + valueToStringConverter: ValueToStringConverter; +}): string | null => { + const { toastNotifications } = services; + + const result = valueToStringConverter(rowIndex, columnId); + const valueFormatted = result.formattedString; + + const copied = copyToClipboard(valueFormatted); + + if (!copied) { + toastNotifications.addWarning({ + title: COPY_FAILED_ERROR_MESSAGE, + }); + + return null; + } + + const toastTitle = i18n.translate('discover.grid.copyValueToClipboard.toastTitle', { + defaultMessage: 'Copied to clipboard', + }); + + if (result.withFormula) { + toastNotifications.addWarning({ + title: toastTitle, + text: WARNING_FOR_FORMULAS, + }); + } else { + toastNotifications.addInfo({ + title: toastTitle, + }); + } + + return valueFormatted; +}; + +export const copyColumnValuesToClipboard = async ({ + columnId, + services, + valueToStringConverter, + rowsCount, +}: { + columnId: string; + services: DiscoverServices; + valueToStringConverter: ValueToStringConverter; + rowsCount: number; +}): Promise => { + const { toastNotifications } = services; + const nameFormattedResult = convertNameToString(columnId); + let withFormula = nameFormattedResult.withFormula; + + const valuesFormatted = [...Array(rowsCount)].map((_, rowIndex) => { + const result = valueToStringConverter(rowIndex, columnId, { disableMultiline: true }); + withFormula = withFormula || result.withFormula; + return result.formattedString; + }); + + const textToCopy = `${nameFormattedResult.formattedString}\n${valuesFormatted.join('\n')}`; + + let copied; + try { + // try to copy without browser styles + await window.navigator?.clipboard?.writeText(textToCopy); + copied = true; + } catch (error) { + copied = copyToClipboard(textToCopy); + } + + if (!copied) { + toastNotifications.addWarning({ + title: COPY_FAILED_ERROR_MESSAGE, + }); + + return null; + } + + const toastTitle = i18n.translate('discover.grid.copyColumnValuesToClipboard.toastTitle', { + defaultMessage: 'Copied values of "{column}" column to clipboard', + values: { column: columnId }, + }); + + if (withFormula) { + toastNotifications.addWarning({ + title: toastTitle, + text: WARNING_FOR_FORMULAS, + }); + } else { + toastNotifications.addInfo({ + title: toastTitle, + }); + } + + return textToCopy; +}; + +export const copyColumnNameToClipboard = ({ + columnId, + services, +}: { + columnId: string; + services: DiscoverServices; +}): string | null => { + const { toastNotifications } = services; + + const nameFormattedResult = convertNameToString(columnId); + const textToCopy = nameFormattedResult.formattedString; + const copied = copyToClipboard(textToCopy); + + if (!copied) { + toastNotifications.addWarning({ + title: COPY_FAILED_ERROR_MESSAGE, + }); + + return null; + } + + const toastTitle = i18n.translate('discover.grid.copyColumnNameToClipboard.toastTitle', { + defaultMessage: 'Copied to clipboard', + }); + + if (nameFormattedResult.withFormula) { + toastNotifications.addWarning({ + title: toastTitle, + text: WARNING_FOR_FORMULAS, + }); + } else { + toastNotifications.addInfo({ + title: toastTitle, + }); + } + + return textToCopy; +}; diff --git a/src/plugins/discover/public/utils/format_value.ts b/src/plugins/discover/public/utils/format_value.ts index b7ee9af7f6873..b2d81899c4b6d 100644 --- a/src/plugins/discover/public/utils/format_value.ts +++ b/src/plugins/discover/public/utils/format_value.ts @@ -10,7 +10,11 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; -import { FieldFormatsContentType } from '@kbn/field-formats-plugin/common/types'; +import type { + FieldFormatsContentType, + HtmlContextTypeOptions, + TextContextTypeOptions, +} from '@kbn/field-formats-plugin/common/types'; /** * Formats the value of a specific field using the appropriate field formatter if available @@ -18,8 +22,11 @@ import { FieldFormatsContentType } from '@kbn/field-formats-plugin/common/types' * * @param value The value to format * @param hit The actual search hit (required to get highlight information from) + * @param fieldFormats Field formatters * @param dataView The data view if available * @param field The field that value was from if available + * @param contentType Type of a converter + * @param options Options for the converter * @returns An sanitized HTML string, that is safe to be applied via dangerouslySetInnerHTML */ export function formatFieldValue( @@ -28,17 +35,24 @@ export function formatFieldValue( fieldFormats: FieldFormatsStart, dataView?: DataView, field?: DataViewField, - contentType?: FieldFormatsContentType | undefined + contentType?: FieldFormatsContentType, + options?: HtmlContextTypeOptions | TextContextTypeOptions ): string { const usedContentType = contentType ?? 'html'; + const converterOptions: HtmlContextTypeOptions | TextContextTypeOptions = { + hit, + field, + ...options, + }; + if (!dataView || !field) { // If either no field is available or no data view, we'll use the default // string formatter to format that field. return fieldFormats .getDefaultInstance(KBN_FIELD_TYPES.STRING) - .convert(value, usedContentType, { hit, field }); + .convert(value, usedContentType, converterOptions); } // If we have a data view and field we use that fields field formatter - return dataView.getFormatterForField(field).convert(value, usedContentType, { hit, field }); + return dataView.getFormatterForField(field).convert(value, usedContentType, converterOptions); } diff --git a/src/plugins/discover/public/utils/get_sharing_data.test.ts b/src/plugins/discover/public/utils/get_sharing_data.test.ts index 63031f689fb33..a1334e3b1b9fa 100644 --- a/src/plugins/discover/public/utils/get_sharing_data.test.ts +++ b/src/plugins/discover/public/utils/get_sharing_data.test.ts @@ -146,13 +146,13 @@ describe('getSharingData', () => { services ); expect(getSearchSource().fields).toStrictEqual([ - 'cool-timefield', - 'cool-field-1', - 'cool-field-2', - 'cool-field-3', - 'cool-field-4', - 'cool-field-5', - 'cool-field-6', + { field: 'cool-timefield', include_unmapped: 'true' }, + { field: 'cool-field-1', include_unmapped: 'true' }, + { field: 'cool-field-2', include_unmapped: 'true' }, + { field: 'cool-field-3', include_unmapped: 'true' }, + { field: 'cool-field-4', include_unmapped: 'true' }, + { field: 'cool-field-5', include_unmapped: 'true' }, + { field: 'cool-field-6', include_unmapped: 'true' }, ]); }); diff --git a/src/plugins/discover/public/utils/get_sharing_data.ts b/src/plugins/discover/public/utils/get_sharing_data.ts index 2e3e385ce71ff..66a6a6f1eed10 100644 --- a/src/plugins/discover/public/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/utils/get_sharing_data.ts @@ -96,7 +96,10 @@ export async function getSharingData( */ const useFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE); if (useFieldsApi && columns.length) { - searchSource.setField('fields', columns); + searchSource.setField( + 'fields', + columns.map((field) => ({ field, include_unmapped: 'true' })) + ); } return searchSource.getSerializedFields(true); }, diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index 193aec8ee5234..37689abe3a3d6 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -118,6 +118,7 @@ export class EmbeddableStateTransfer { options?: { path?: string; openInNewTab?: boolean; + skipAppLeave?: boolean; state: EmbeddableEditorState; } ): Promise { @@ -165,7 +166,12 @@ export class EmbeddableStateTransfer { private async navigateToWithState( appId: string, key: string, - options?: { path?: string; state?: OutgoingStateType; openInNewTab?: boolean } + options?: { + path?: string; + state?: OutgoingStateType; + openInNewTab?: boolean; + skipAppLeave?: boolean; + } ): Promise { const existingAppState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key] || {}; const stateObject = { @@ -176,6 +182,10 @@ export class EmbeddableStateTransfer { }, }; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject); - await this.navigateToApp(appId, { path: options?.path, openInNewTab: options?.openInNewTab }); + await this.navigateToApp(appId, { + path: options?.path, + openInNewTab: options?.openInNewTab, + skipAppLeave: options?.skipAppLeave, + }); } } diff --git a/src/plugins/expressions/public/loader.test.ts b/src/plugins/expressions/public/loader.test.ts index 06cb30a74fe97..f2d156c4af681 100644 --- a/src/plugins/expressions/public/loader.test.ts +++ b/src/plugins/expressions/public/loader.test.ts @@ -154,12 +154,12 @@ describe('ExpressionLoader', () => { it('throttles partial results', async () => { testScheduler.run(({ cold, expectObservable }) => { const expressionLoader = new ExpressionLoader(element, 'var foo', { - variables: { foo: cold('a 5ms b 5ms c 10ms d', { a: 1, b: 2, c: 3, d: 4 }) }, + variables: { foo: cold('a 5ms b 5ms c 10ms (d|)', { a: 1, b: 2, c: 3, d: 4 }) }, partial: true, throttle: 20, }); - expectObservable(expressionLoader.data$).toBe('a 19ms c 19ms d', { + expectObservable(expressionLoader.data$).toBe('a 19ms c 2ms d', { a: expect.objectContaining({ result: 1 }), c: expect.objectContaining({ result: 3 }), d: expect.objectContaining({ result: 4 }), diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 7f7a96fde6f1a..afbde2ab3043a 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { BehaviorSubject, Observable, Subject, Subscription, asyncScheduler, identity } from 'rxjs'; -import { filter, map, delay, shareReplay, throttleTime } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subject, Subscription, identity, timer } from 'rxjs'; +import { delay, filter, finalize, map, shareReplay, takeWhile } from 'rxjs/operators'; import { defaults } from 'lodash'; import { SerializableRecord, UnwrapObservable } from '@kbn/utility-types'; import { Adapters } from '@kbn/inspector-plugin/public'; @@ -20,6 +20,61 @@ import { getExpressionsService } from './services'; type Data = unknown; +/** + * RxJS' `throttle` operator does not emit the last value immediately when the source observable is completed. + * Instead, it waits for the next throttle period to emit that. + * It might cause delays until we get the final value, even though it is already there. + * @see https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/throttle.ts#L121 + */ +function throttle(timeout: number) { + return (source: Observable): Observable => + new Observable((subscriber) => { + let latest: T | undefined; + let hasValue = false; + + const emit = () => { + if (hasValue) { + subscriber.next(latest); + hasValue = false; + latest = undefined; + } + }; + + let throttled: Subscription | undefined; + const timer$ = timer(0, timeout).pipe( + takeWhile(() => hasValue), + finalize(() => { + subscriber.remove(throttled!); + throttled = undefined; + }) + ); + + subscriber.add( + source.subscribe({ + next: (value) => { + latest = value; + hasValue = true; + + if (!throttled) { + throttled = timer$.subscribe(emit); + subscriber.add(throttled); + } + }, + error: (error) => subscriber.error(error), + complete: () => { + emit(); + subscriber.complete(); + }, + }) + ); + + subscriber.add(() => { + hasValue = false; + latest = undefined; + }); + }); +} + export class ExpressionLoader { data$: ReturnType; update$: ExpressionRenderHandler['update$']; @@ -151,9 +206,7 @@ export class ExpressionLoader { .pipe( delay(0), // delaying until the next tick since we execute the expression in the constructor filter(({ partial }) => params.partial || !partial), - params.partial && params.throttle - ? throttleTime(params.throttle, asyncScheduler, { leading: true, trailing: true }) - : identity + params.partial && params.throttle ? throttle(params.throttle) : identity ) .subscribe((value) => this.dataSubject.next(value)); }; diff --git a/src/plugins/field_formats/common/content_types/html_content_type.ts b/src/plugins/field_formats/common/content_types/html_content_type.ts index d8d664e1d1b64..f1906ab3e68f7 100644 --- a/src/plugins/field_formats/common/content_types/html_content_type.ts +++ b/src/plugins/field_formats/common/content_types/html_content_type.ts @@ -36,7 +36,7 @@ export const setup = ( const recurse: HtmlContextTypeConvert = (value, options = {}) => { if (value == null) { - return asPrettyString(value); + return asPrettyString(value, options); } if (!value || !isFunction(value.map)) { diff --git a/src/plugins/field_formats/common/converters/boolean.ts b/src/plugins/field_formats/common/converters/boolean.ts index 81fd419c9861e..a44540b639032 100644 --- a/src/plugins/field_formats/common/converters/boolean.ts +++ b/src/plugins/field_formats/common/converters/boolean.ts @@ -20,7 +20,7 @@ export class BoolFormat extends FieldFormat { }); static fieldType = [KBN_FIELD_TYPES.BOOLEAN, KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING]; - textConvert: TextContextTypeConvert = (value: string | number | boolean) => { + textConvert: TextContextTypeConvert = (value: string | number | boolean, options) => { if (typeof value === 'string') { value = value.trim().toLowerCase(); } @@ -37,7 +37,7 @@ export class BoolFormat extends FieldFormat { case 'yes': return 'true'; default: - return asPrettyString(value); + return asPrettyString(value, options); } }; } diff --git a/src/plugins/field_formats/common/converters/color.tsx b/src/plugins/field_formats/common/converters/color.tsx index 197468fc1592a..9bbc66e662d9a 100644 --- a/src/plugins/field_formats/common/converters/color.tsx +++ b/src/plugins/field_formats/common/converters/color.tsx @@ -54,10 +54,10 @@ export class ColorFormat extends FieldFormat { } } - htmlConvert: HtmlContextTypeConvert = (val: string | number) => { + htmlConvert: HtmlContextTypeConvert = (val: string | number, options) => { const color = this.findColorRuleForVal(val) as typeof DEFAULT_CONVERTER_COLOR; - const displayVal = escape(asPrettyString(val)); + const displayVal = escape(asPrettyString(val, options)); if (!color) return displayVal; return ReactDOM.renderToStaticMarkup( diff --git a/src/plugins/field_formats/common/converters/geo_point.ts b/src/plugins/field_formats/common/converters/geo_point.ts index 68fbd74096d62..a5e77db35589d 100644 --- a/src/plugins/field_formats/common/converters/geo_point.ts +++ b/src/plugins/field_formats/common/converters/geo_point.ts @@ -95,14 +95,17 @@ export class GeoPointFormat extends FieldFormat { }; } - textConvert: TextContextTypeConvert = (val: Point | { lat: number; lon: number } | string) => { + textConvert: TextContextTypeConvert = ( + val: Point | { lat: number; lon: number } | string, + options + ) => { if (!val) { return ''; } const point: Point | null = isPoint(val) ? (val as Point) : toPoint(val); if (!point) { - return asPrettyString(val); + return asPrettyString(val, options); } switch (this.param('transform')) { @@ -111,7 +114,7 @@ export class GeoPointFormat extends FieldFormat { case 'wkt': return `POINT (${point.coordinates[0]} ${point.coordinates[1]})`; default: - return asPrettyString(val); + return asPrettyString(val, options); } }; } diff --git a/src/plugins/field_formats/common/converters/histogram.ts b/src/plugins/field_formats/common/converters/histogram.ts index 2f6928b2abd8e..cdfcb52cb85be 100644 --- a/src/plugins/field_formats/common/converters/histogram.ts +++ b/src/plugins/field_formats/common/converters/histogram.ts @@ -34,7 +34,7 @@ export class HistogramFormat extends FieldFormat { }; } - textConvert: TextContextTypeConvert = (val: number) => { + textConvert: TextContextTypeConvert = (val: number, options) => { if (typeof val === 'number') { const subFormatId = this.param('id'); const SubFormat = @@ -44,7 +44,7 @@ export class HistogramFormat extends FieldFormat { ? PercentFormat : NumberFormat; const converter = new SubFormat(this.param('params'), this.getConfig); - return converter.textConvert(val); + return converter.textConvert(val, options); } else { return JSON.stringify(val); } diff --git a/src/plugins/field_formats/common/converters/string.ts b/src/plugins/field_formats/common/converters/string.ts index e3700e1b52429..e6e88753e782b 100644 --- a/src/plugins/field_formats/common/converters/string.ts +++ b/src/plugins/field_formats/common/converters/string.ts @@ -109,7 +109,7 @@ export class StringFormat extends FieldFormat { }); } - textConvert: TextContextTypeConvert = (val: string | number) => { + textConvert: TextContextTypeConvert = (val: string | number, options) => { if (val === '') { return emptyLabel; } @@ -121,13 +121,13 @@ export class StringFormat extends FieldFormat { case 'title': return this.toTitleCase(String(val)); case 'short': - return asPrettyString(shortenDottedString(val)); + return asPrettyString(shortenDottedString(val), options); case 'base64': return this.base64Decode(String(val)); case 'urlparam': return decodeURIComponent(String(val)); default: - return asPrettyString(val); + return asPrettyString(val, options); } }; diff --git a/src/plugins/field_formats/common/field_format.test.ts b/src/plugins/field_formats/common/field_format.test.ts index 1068c2455f96e..41c403d8055c7 100644 --- a/src/plugins/field_formats/common/field_format.test.ts +++ b/src/plugins/field_formats/common/field_format.test.ts @@ -9,11 +9,11 @@ import { constant, trimEnd, trimStart, get } from 'lodash'; import { FieldFormat } from './field_format'; import { asPrettyString } from './utils'; -import { FieldFormatParams } from './types'; +import { FieldFormatParams, TextContextTypeOptions } from './types'; const getTestFormat = ( _params?: FieldFormatParams, - textConvert = (val: string) => asPrettyString(val), + textConvert = (val: string, options?: TextContextTypeOptions) => asPrettyString(val, options), htmlConvert?: (val: string) => string ) => new (class TestFormat extends FieldFormat { diff --git a/src/plugins/field_formats/common/field_format.ts b/src/plugins/field_formats/common/field_format.ts index 05b376ddf3272..9b783b244e868 100644 --- a/src/plugins/field_formats/common/field_format.ts +++ b/src/plugins/field_formats/common/field_format.ts @@ -192,7 +192,7 @@ export abstract class FieldFormat { const params = transform( this._params, - (uniqParams: FieldFormatParams & FieldFormatMetaParams, val: unknown, param: string) => { + (uniqParams: FieldFormatParams & FieldFormatMetaParams, val, param: string) => { if (param === 'parsedUrl') return; if (param && val !== get(defaultsParams, param)) { uniqParams[param] = val; diff --git a/src/plugins/field_formats/common/types.ts b/src/plugins/field_formats/common/types.ts index 55db21ffa74ea..04c655c972af0 100644 --- a/src/plugins/field_formats/common/types.ts +++ b/src/plugins/field_formats/common/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Serializable, SerializableRecord } from '@kbn/utility-types'; import { FieldFormat } from './field_format'; import { FieldFormatsRegistry } from './field_formats_registry'; @@ -17,7 +18,8 @@ export type FieldFormatsContentType = 'html' | 'text'; */ export interface HtmlContextTypeOptions { field?: { name: string }; - hit?: { highlight: Record }; + hit?: { highlight?: Record }; + skipFormattingInStringifiedJSON?: boolean; } /** @@ -29,9 +31,11 @@ export type HtmlContextTypeConvert = (value: any, options?: HtmlContextTypeOptio /** * Plain text converter options * @remark - * no options for now */ -export type TextContextTypeOptions = object; +export interface TextContextTypeOptions { + skipFormattingInStringifiedJSON?: boolean; + timezone?: string; +} /** * To plain text converter function @@ -74,11 +78,12 @@ export enum FIELD_FORMAT_IDS { } /** @public */ -export interface FieldFormatConfig { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type FieldFormatConfig = { id: FieldFormatId; params: FieldFormatParams; es?: boolean; -} +}; /** * If a service is being shared on both the client and the server, and @@ -92,7 +97,10 @@ export interface FieldFormatConfig { * @public */ -export type FieldFormatsGetConfigFn = (key: string, defaultOverride?: T) => T; +export type FieldFormatsGetConfigFn = ( + key: string, + defaultOverride?: T +) => T; export type IFieldFormat = FieldFormat; @@ -123,9 +131,7 @@ export type FieldFormatInstanceType = (new ( * TODO: support strict typing for params depending on format type * https://github.com/elastic/kibana/issues/108158 */ -export interface FieldFormatParams { - [param: string]: any; -} +export type FieldFormatParams = SerializableRecord; /** * Params provided by the registry to every field formatter @@ -149,9 +155,10 @@ export type FieldFormatsStartCommon = Omit { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SerializedFieldFormat = { id?: string; params?: TParams; -} +}; export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; diff --git a/src/plugins/field_formats/common/utils/as_pretty_string.test.ts b/src/plugins/field_formats/common/utils/as_pretty_string.test.ts index 2505a490bb79a..09d966804436a 100644 --- a/src/plugins/field_formats/common/utils/as_pretty_string.test.ts +++ b/src/plugins/field_formats/common/utils/as_pretty_string.test.ts @@ -21,6 +21,9 @@ describe('asPrettyString', () => { test('Converts objects values into presentable strings', () => { expect(asPrettyString({ key: 'value' })).toBe('{\n "key": "value"\n}'); + expect(asPrettyString({ key: 'value' }, { skipFormattingInStringifiedJSON: true })).toBe( + '{"key":"value"}' + ); }); test('Converts other non-string values into strings', () => { diff --git a/src/plugins/field_formats/common/utils/as_pretty_string.ts b/src/plugins/field_formats/common/utils/as_pretty_string.ts index 95424f00b2c44..61f80e5645623 100644 --- a/src/plugins/field_formats/common/utils/as_pretty_string.ts +++ b/src/plugins/field_formats/common/utils/as_pretty_string.ts @@ -9,13 +9,18 @@ /** * Convert a value to a presentable string */ -export function asPrettyString(val: unknown): string { +export function asPrettyString( + val: unknown, + options?: { skipFormattingInStringifiedJSON?: boolean } +): string { if (val === null || val === undefined) return ' - '; switch (typeof val) { case 'string': return val; case 'object': - return JSON.stringify(val, null, ' '); + return options?.skipFormattingInStringifiedJSON + ? JSON.stringify(val) + : JSON.stringify(val, null, ' '); default: return '' + val; } diff --git a/src/plugins/field_formats/server/lib/converters/date_nanos_server.ts b/src/plugins/field_formats/server/lib/converters/date_nanos_server.ts index 3a04dd2353b88..9c2e7f8dc75d7 100644 --- a/src/plugins/field_formats/server/lib/converters/date_nanos_server.ts +++ b/src/plugins/field_formats/server/lib/converters/date_nanos_server.ts @@ -16,7 +16,7 @@ import { import { TextContextTypeConvert } from '../../../common/types'; class DateNanosFormatServer extends DateNanosFormat { - textConvert: TextContextTypeConvert = (val: string | number, options?: { timezone?: string }) => { + textConvert: TextContextTypeConvert = (val: string | number, options) => { // don't give away our ref to converter so // we can hot-swap when config changes const pattern = this.param('pattern'); diff --git a/src/plugins/field_formats/server/lib/converters/date_server.ts b/src/plugins/field_formats/server/lib/converters/date_server.ts index 7cfd8ff7c9b4e..e5f7cad713aae 100644 --- a/src/plugins/field_formats/server/lib/converters/date_server.ts +++ b/src/plugins/field_formats/server/lib/converters/date_server.ts @@ -68,7 +68,7 @@ export class DateFormat extends FieldFormat { }; } - textConvert: TextContextTypeConvert = (val: string | number, options?: { timezone?: string }) => { + textConvert: TextContextTypeConvert = (val: string | number, options) => { // don't give away our ref to converter so we can hot-swap when config changes const pattern = this.param('pattern'); const timezone = options?.timezone || this.param('timezone'); diff --git a/src/plugins/home/server/services/sample_data/sample_data_registry.test.ts b/src/plugins/home/server/services/sample_data/sample_data_registry.test.ts index 30aa2d7e48d2f..2ba6a4172fbcb 100644 --- a/src/plugins/home/server/services/sample_data/sample_data_registry.test.ts +++ b/src/plugins/home/server/services/sample_data/sample_data_registry.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { CoreSetup } from '@kbn/core/server'; import { CustomIntegrationsPluginSetup } from '@kbn/custom-integrations-plugin/server'; diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts index 537e6267d0293..7046500dc717e 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { TutorialsRegistry } from './tutorials_registry'; import { coreMock } from '@kbn/core/server/mocks'; import { CoreSetup } from '@kbn/core/server'; diff --git a/src/plugins/inspector/public/views/requests/components/disambiguate_request_names.test.ts b/src/plugins/inspector/public/views/requests/components/disambiguate_request_names.test.ts new file mode 100644 index 0000000000000..fe7141667b34f --- /dev/null +++ b/src/plugins/inspector/public/views/requests/components/disambiguate_request_names.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Request } from '../../../../common/adapters/request/types'; +import { disambiguateRequestNames } from './disambiguate_request_names'; + +describe('disambiguateRequestNames', () => { + test('correctly disambiguates request names and preserves order', () => { + const requests = [ + { + id: '1', + name: 'Name A', + }, + { + id: '2', + name: 'Name B', + }, + { + id: '3', + name: 'Name A', + }, + { + id: '4', + name: 'Name C', + }, + { + id: '5', + name: 'Name B', + }, + { + id: '6', + name: 'Name A', + }, + ] as Request[]; + + expect(disambiguateRequestNames(requests)).toEqual([ + { + id: '1', + name: 'Name A (1)', + }, + { + id: '2', + name: 'Name B (1)', + }, + { + id: '3', + name: 'Name A (2)', + }, + { + id: '4', + name: 'Name C', + }, + { + id: '5', + name: 'Name B (2)', + }, + { + id: '6', + name: 'Name A (3)', + }, + ]); + }); + + test('does not change names unnecessarily', () => { + const requests = [ + { + id: '1', + name: 'Test 1', + }, + { + id: '2', + name: 'Test 2', + }, + { + id: '3', + name: 'Test 3', + }, + ] as Request[]; + + expect(disambiguateRequestNames(requests)).toEqual(requests); + }); + + test('correctly handles empty arrays', () => { + expect(disambiguateRequestNames([])).toEqual([]); + }); +}); diff --git a/src/plugins/inspector/public/views/requests/components/disambiguate_request_names.ts b/src/plugins/inspector/public/views/requests/components/disambiguate_request_names.ts new file mode 100644 index 0000000000000..c10355a7400fb --- /dev/null +++ b/src/plugins/inspector/public/views/requests/components/disambiguate_request_names.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 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 { groupBy } from 'lodash'; +import type { Request } from '../../../../common/adapters/request/types'; + +export function disambiguateRequestNames(requests: Request[]): Request[] { + const requestsByName = groupBy(requests, (r) => r.name); + + const newNamesById = Object.entries(requestsByName).reduce<{ [requestId: string]: string }>( + (acc, [name, reqs]) => { + const moreThanOne = reqs.length > 1; + reqs.forEach((req, idx) => { + const id = req.id; + acc[id] = moreThanOne ? `${name} (${idx + 1})` : name; + }); + return acc; + }, + {} + ); + + return requests.map((request) => ({ + ...request, + name: newNamesById[request.id], + })); +} diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx index 822175cc65eea..a88bb9b768142 100644 --- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx @@ -17,6 +17,7 @@ import { InspectorViewProps } from '../../../types'; import { RequestSelector } from './request_selector'; import { RequestDetails } from './request_details'; +import { disambiguateRequestNames } from './disambiguate_request_names'; interface RequestSelectorState { requests: Request[]; @@ -34,15 +35,19 @@ export class RequestsViewComponent extends Component { - const requests = this.props.adapters.requests!.getRequests(); + const requests = this.getRequests(); const newState = { requests } as RequestSelectorState; if (!this.state.request || !requests.includes(this.state.request)) { diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 30d5d4ff1b373..8a2ca233d6930 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -26,7 +26,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; import { ThemeServiceStart, HttpFetchError, ToastsStart, ApplicationStart } from '@kbn/core/public'; -import { debounce, keyBy, sortBy, uniq } from 'lodash'; +import { debounce, keyBy, sortBy, uniq, get } from 'lodash'; import React from 'react'; import moment from 'moment'; import { KibanaPageTemplate } from '../page_template'; @@ -583,9 +583,13 @@ class TableListView extends React.Component< if (this.props.editItem) { const actions: EuiTableActionsColumnType['actions'] = [ { - name: i18n.translate('kibana-react.tableListView.listing.table.editActionName', { - defaultMessage: 'Edit', - }), + name: (item) => + i18n.translate('kibana-react.tableListView.listing.table.editActionName', { + defaultMessage: 'Edit {itemDescription}', + values: { + itemDescription: get(item, this.props.rowHeader), + }, + }), description: i18n.translate( 'kibana-react.tableListView.listing.table.editActionDescription', { diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index e650dc5bbc3c4..bd21e700c5990 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -134,7 +134,6 @@ export const applicationUsageSchema = { apm: commonSchema, canvas: commonSchema, enterpriseSearch: commonSchema, - enterpriseSearchContent: commonSchema, elasticsearch: commonSchema, appSearch: commonSchema, workplaceSearch: commonSchema, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts index b18fed38ef9e4..1e99e55779a04 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts @@ -12,6 +12,11 @@ import { UsageStats } from './types'; import { REDACTED_KEYWORD } from '../../../common/constants'; import { stackManagementSchema } from './schema'; +/** + * These config keys should be redacted from any usage data, they are only used for implementation details of the config saved object. + */ +const CONFIG_KEYS_TO_REDACT = ['buildNum', 'isDefaultIndexMigrated']; + export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClient | undefined) { return async function fetchUsageStats(): Promise { const uiSettingsClient = getUiSettingsClient(); @@ -21,7 +26,7 @@ export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClien const userProvided = await uiSettingsClient.getUserProvided(); const modifiedEntries = Object.entries(userProvided) - .filter(([key]) => key !== 'buildNum') + .filter(([key]) => !CONFIG_KEYS_TO_REDACT.includes(key)) .reduce((obj: Record, [key, { userValue }]) => { const sensitive = uiSettingsClient.isSensitive(key); obj[key] = sensitive ? REDACTED_KEYWORD : userValue; diff --git a/src/plugins/kibana_utils/common/persistable_state/types.ts b/src/plugins/kibana_utils/common/persistable_state/types.ts index 4db588d15741f..31234ca5c18da 100644 --- a/src/plugins/kibana_utils/common/persistable_state/types.ts +++ b/src/plugins/kibana_utils/common/persistable_state/types.ts @@ -159,7 +159,7 @@ export interface PersistableStateService

* @param version Current semver version of the `state`. * @returns A serializable state object migrated to the latest state. */ - migrateToLatest?: (state: VersionedState) => P; + migrateToLatest?: (state: VersionedState

) => P; /** * returns all registered migrations diff --git a/src/plugins/kibana_utils/public/storage/storage.test.ts b/src/plugins/kibana_utils/public/storage/storage.test.ts index ed9b3ca9593d3..3e9423c41e5d8 100644 --- a/src/plugins/kibana_utils/public/storage/storage.test.ts +++ b/src/plugins/kibana_utils/public/storage/storage.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { Storage } from './storage'; import { IStorage, IStorageWrapper } from './types'; diff --git a/src/plugins/screenshot_mode/public/mocks.ts b/src/plugins/screenshot_mode/public/mocks.ts index 86ef4c3cf1a42..1d3e226a83b6b 100644 --- a/src/plugins/screenshot_mode/public/mocks.ts +++ b/src/plugins/screenshot_mode/public/mocks.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types'; export const screenshotModePluginMock = { diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index f95e75e528d80..19ebcfbe0fef9 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -2223,137 +2223,6 @@ } } }, - "enterpriseSearchContent": { - "properties": { - "appId": { - "type": "keyword", - "_meta": { - "description": "The application being tracked" - } - }, - "viewId": { - "type": "keyword", - "_meta": { - "description": "Always `main`" - } - }, - "clicks_total": { - "type": "long", - "_meta": { - "description": "General number of clicks in the application since we started counting them" - } - }, - "clicks_7_days": { - "type": "long", - "_meta": { - "description": "General number of clicks in the application over the last 7 days" - } - }, - "clicks_30_days": { - "type": "long", - "_meta": { - "description": "General number of clicks in the application over the last 30 days" - } - }, - "clicks_90_days": { - "type": "long", - "_meta": { - "description": "General number of clicks in the application over the last 90 days" - } - }, - "minutes_on_screen_total": { - "type": "float", - "_meta": { - "description": "Minutes the application is active and on-screen since we started counting them." - } - }, - "minutes_on_screen_7_days": { - "type": "float", - "_meta": { - "description": "Minutes the application is active and on-screen over the last 7 days" - } - }, - "minutes_on_screen_30_days": { - "type": "float", - "_meta": { - "description": "Minutes the application is active and on-screen over the last 30 days" - } - }, - "minutes_on_screen_90_days": { - "type": "float", - "_meta": { - "description": "Minutes the application is active and on-screen over the last 90 days" - } - }, - "views": { - "type": "array", - "items": { - "properties": { - "appId": { - "type": "keyword", - "_meta": { - "description": "The application being tracked" - } - }, - "viewId": { - "type": "keyword", - "_meta": { - "description": "The application view being tracked" - } - }, - "clicks_total": { - "type": "long", - "_meta": { - "description": "General number of clicks in the application sub view since we started counting them" - } - }, - "clicks_7_days": { - "type": "long", - "_meta": { - "description": "General number of clicks in the active application sub view over the last 7 days" - } - }, - "clicks_30_days": { - "type": "long", - "_meta": { - "description": "General number of clicks in the active application sub view over the last 30 days" - } - }, - "clicks_90_days": { - "type": "long", - "_meta": { - "description": "General number of clicks in the active application sub view over the last 90 days" - } - }, - "minutes_on_screen_total": { - "type": "float", - "_meta": { - "description": "Minutes the application sub view is active and on-screen since we started counting them." - } - }, - "minutes_on_screen_7_days": { - "type": "float", - "_meta": { - "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" - } - }, - "minutes_on_screen_30_days": { - "type": "float", - "_meta": { - "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" - } - }, - "minutes_on_screen_90_days": { - "type": "float", - "_meta": { - "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" - } - } - } - } - } - } - }, "elasticsearch": { "properties": { "appId": { diff --git a/x-pack/plugins/ui_actions_enhanced/.eslintrc.json b/src/plugins/ui_actions_enhanced/.eslintrc.json similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/.eslintrc.json rename to src/plugins/ui_actions_enhanced/.eslintrc.json diff --git a/src/plugins/ui_actions_enhanced/.storybook/main.js b/src/plugins/ui_actions_enhanced/.storybook/main.js new file mode 100644 index 0000000000000..8dc3c5d1518f4 --- /dev/null +++ b/src/plugins/ui_actions_enhanced/.storybook/main.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/x-pack/plugins/ui_actions_enhanced/README.md b/src/plugins/ui_actions_enhanced/README.md similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/README.md rename to src/plugins/ui_actions_enhanced/README.md diff --git a/x-pack/plugins/ui_actions_enhanced/common/index.ts b/src/plugins/ui_actions_enhanced/common/index.ts similarity index 59% rename from x-pack/plugins/ui_actions_enhanced/common/index.ts rename to src/plugins/ui_actions_enhanced/common/index.ts index 30192e925d6f7..f92a61f3f7847 100644 --- a/x-pack/plugins/ui_actions_enhanced/common/index.ts +++ b/src/plugins/ui_actions_enhanced/common/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export type { diff --git a/x-pack/plugins/ui_actions_enhanced/common/types.ts b/src/plugins/ui_actions_enhanced/common/types.ts similarity index 79% rename from x-pack/plugins/ui_actions_enhanced/common/types.ts rename to src/plugins/ui_actions_enhanced/common/types.ts index 02cab5d17c0b2..5237b6f23ee2c 100644 --- a/x-pack/plugins/ui_actions_enhanced/common/types.ts +++ b/src/plugins/ui_actions_enhanced/common/types.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { SerializableRecord } from '@kbn/utility-types'; diff --git a/src/plugins/ui_actions_enhanced/jest.config.js b/src/plugins/ui_actions_enhanced/jest.config.js new file mode 100644 index 0000000000000..8cd91fb2922b1 --- /dev/null +++ b/src/plugins/ui_actions_enhanced/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/ui_actions_enhanced'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/ui_actions_enhanced', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/ui_actions_enhanced/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/src/plugins/ui_actions_enhanced/kibana.json similarity index 69% rename from x-pack/plugins/ui_actions_enhanced/kibana.json rename to src/plugins/ui_actions_enhanced/kibana.json index fd7ffb820727d..0f050db399d38 100644 --- a/x-pack/plugins/ui_actions_enhanced/kibana.json +++ b/src/plugins/ui_actions_enhanced/kibana.json @@ -6,9 +6,10 @@ }, "description": "Extends UI Actions plugin with more functionality", "version": "kibana", - "configPath": ["xpack", "ui_actions_enhanced"], + "configPath": ["src", "ui_actions_enhanced"], "server": true, "ui": true, - "requiredPlugins": ["embeddable", "uiActions", "licensing"], + "requiredPlugins": ["embeddable", "uiActions"], + "optionalPlugins": ["licensing"], "requiredBundles": ["kibanaUtils", "kibanaReact", "data"] } diff --git a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts b/src/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts similarity index 88% rename from x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts rename to src/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts index 5418f4a8f55d5..1221b44aefa79 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts +++ b/src/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { canInheritTimeRange } from './can_inherit_time_range'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts b/src/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts similarity index 79% rename from x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts rename to src/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts index 9a1f089c08302..3dd8d658b3906 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts +++ b/src/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { Embeddable, IContainer, ContainerInput } from '@kbn/embeddable-plugin/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_factory_picker/action_factory_picker.tsx b/src/plugins/ui_actions_enhanced/public/components/action_factory_picker/action_factory_picker.tsx similarity index 90% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_factory_picker/action_factory_picker.tsx rename to src/plugins/ui_actions_enhanced/public/components/action_factory_picker/action_factory_picker.tsx index dcf99d4001797..b6b41b31b7e4c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_factory_picker/action_factory_picker.tsx +++ b/src/plugins/ui_actions_enhanced/public/components/action_factory_picker/action_factory_picker.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/index.ts b/src/plugins/ui_actions_enhanced/public/components/action_factory_picker/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/index.ts rename to src/plugins/ui_actions_enhanced/public/components/action_factory_picker/index.ts index 3d7ffed301859..47e9321e74814 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/index.ts +++ b/src/plugins/ui_actions_enhanced/public/components/action_factory_picker/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './action_factory_picker'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.scss b/src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.scss similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.scss rename to src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.scss diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx b/src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx similarity index 85% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx rename to src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx index aaf65269a17ef..a4c65d6ccf17c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx +++ b/src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx similarity index 95% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx rename to src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx index 8b5a5c7e6fdde..02041e6e8ed49 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx +++ b/src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx similarity index 97% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx rename to src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index 4d713f2a700c6..c819e922e9b7f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/src/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; @@ -355,7 +356,7 @@ const ActionFactorySelector: React.FC = ({ !actionFactory.isCompatibleLicense() && ( ) } diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts b/src/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts similarity index 66% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts rename to src/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts index 532a230a77273..342a6b9c66120 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts @@ -1,49 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtChangeButton = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.changeButton', + 'uiActionsEnhanced.components.actionWizard.changeButton', { defaultMessage: 'Change', } ); export const txtTriggerPickerLabel = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel', + 'uiActionsEnhanced.components.actionWizard.triggerPickerLabel', { defaultMessage: 'Show option on:', } ); export const txtTriggerPickerHelpText = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText', + 'uiActionsEnhanced.components.actionWizard.triggerPickerHelpText', { defaultMessage: "What's this?", } ); export const txtTriggerPickerHelpTooltip = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip', + 'uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip', { defaultMessage: 'Determines when the drilldown appears in context menu', } ); export const txtBetaActionFactoryLabel = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel', + 'uiActionsEnhanced.components.actionWizard.betaActionLabel', { defaultMessage: `Beta`, } ); export const txtBetaActionFactoryTooltip = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip', + 'uiActionsEnhanced.components.actionWizard.betaActionTooltip', { defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting bugs or providing other feedback.`, } diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/index.ts b/src/plugins/ui_actions_enhanced/public/components/action_wizard/index.ts similarity index 53% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/index.ts rename to src/plugins/ui_actions_enhanced/public/components/action_wizard/index.ts index 70d6d7d2681e3..311e45c4f3cc8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/index.ts +++ b/src/plugins/ui_actions_enhanced/public/components/action_wizard/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export { ActionWizard } from './action_wizard'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/src/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx similarity index 97% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx rename to src/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx index aded7a306a767..9b8c7ebef0dad 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx +++ b/src/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React, { useState } from 'react'; diff --git a/src/plugins/ui_actions_enhanced/public/components/index.ts b/src/plugins/ui_actions_enhanced/public/components/index.ts new file mode 100644 index 0000000000000..14340071e94db --- /dev/null +++ b/src/plugins/ui_actions_enhanced/public/components/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +export * from './action_wizard'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts similarity index 67% rename from x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts rename to src/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts index 2d139095e81d7..636e4efa16cee 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts @@ -1,28 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtBetaActionFactoryLabel = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel', + 'uiActionsEnhanced.components.actionWizard.betaActionLabel', { defaultMessage: `Beta`, } ); export const txtBetaActionFactoryTooltip = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip', + 'uiActionsEnhanced.components.actionWizard.betaActionTooltip', { defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting bugs or providing other feedback.`, } ); export const txtInsufficientLicenseLevel = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip', + 'uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip', { defaultMessage: 'Insufficient license level', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/index.ts b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/index.ts similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/index.ts rename to src/plugins/ui_actions_enhanced/public/components/presentable_picker/index.ts index 0259ac6556e64..3197d9a71b312 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/index.ts +++ b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './presentable_picker'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.stories.tsx b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.stories.tsx similarity index 95% rename from x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.stories.tsx rename to src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.stories.tsx index 521d16ad579c1..2cef67c96ab8f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.stories.tsx +++ b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.stories.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.tsx b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.tsx similarity index 90% rename from x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.tsx rename to src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.tsx index 9667a78df9a4e..e8a5bbd1da35c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.tsx +++ b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker_item.tsx b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker_item.tsx similarity index 90% rename from x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker_item.tsx rename to src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker_item.tsx index 76d686b14f869..7c14bfe25d9e8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker_item.tsx +++ b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/presentable_picker_item.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/styles.scss b/src/plugins/ui_actions_enhanced/public/components/presentable_picker/styles.scss similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/styles.scss rename to src/plugins/ui_actions_enhanced/public/components/presentable_picker/styles.scss diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts b/src/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts similarity index 97% rename from x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts rename to src/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts index e1c6a1aa095e2..78be66fa84fb4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts +++ b/src/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx similarity index 92% rename from x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx rename to src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index 58b30c0f2927b..df22733a4d494 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ b/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { i18n } from '@kbn/i18n'; @@ -66,7 +67,7 @@ export class CustomTimeRangeAction implements Action { } public getDisplayName() { - return i18n.translate('xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName', { + return i18n.translate('uiActionsEnhanced.customizeTimeRangeMenuItem.displayName', { defaultMessage: 'Customize time range', }); } diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts b/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts similarity index 96% rename from x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts rename to src/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts index f31e2552fa717..3cc4c3b44cdc7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts +++ b/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx b/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx similarity index 94% rename from x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx rename to src/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx index 8f834b439b55b..4f5b5b6cb6be8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx +++ b/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx b/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx similarity index 83% rename from x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx rename to src/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx index 9dc7bd017740c..d73b3ea59b708 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx +++ b/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx b/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx similarity index 87% rename from x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx rename to src/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx index f253e2f3de059..57b8a433d42ef 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx +++ b/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React, { Component } from 'react'; @@ -96,7 +97,7 @@ export class CustomizeTimeRangeModal extends Component - {i18n.translate('xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle', { + {i18n.translate('uiActionsEnhanced.customizeTimeRange.modal.headerTitle', { defaultMessage: 'Customize panel time range', })} @@ -105,7 +106,7 @@ export class CustomizeTimeRangeModal extends Component {i18n.translate( - 'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle', + 'uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle', { defaultMessage: 'Remove', } @@ -153,7 +154,7 @@ export class CustomizeTimeRangeModal extends Component {i18n.translate( - 'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle', + 'uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle', { defaultMessage: 'Cancel', } @@ -164,13 +165,13 @@ export class CustomizeTimeRangeModal extends Component {this.state.inheritTimeRange ? i18n.translate( - 'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle', + 'uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle', { defaultMessage: 'Add to panel', } ) : i18n.translate( - 'xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle', + 'uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle', { defaultMessage: 'Update', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts b/src/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts similarity index 82% rename from x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts rename to src/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts index 58fe86c117f0c..52b3abc6ebbdd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts +++ b/src/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { Embeddable, IContainer, ContainerInput } from '@kbn/embeddable-plugin/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts similarity index 96% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index 4928b368a96b4..1efe08490bf30 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { LicenseType } from '@kbn/licensing-plugin/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/README.md b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/README.md similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/README.md rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/README.md diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx similarity index 83% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx index 7ee9fe51e59b9..9ed41740402bd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { @@ -19,39 +20,36 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; const txtDrilldownAction = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownForm.drilldownAction', + 'uiActionsEnhanced.components.DrilldownForm.drilldownAction', { defaultMessage: 'Action', } ); const txtGetMoreActions = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel', + 'uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel', { defaultMessage: 'Get more actions', } ); const txtBetaActionFactoryLabel = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownForm.betaActionLabel', + 'uiActionsEnhanced.components.DrilldownForm.betaActionLabel', { defaultMessage: `Beta`, } ); const txtBetaActionFactoryTooltip = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownForm.betaActionTooltip', + 'uiActionsEnhanced.components.DrilldownForm.betaActionTooltip', { defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting bugs or providing other feedback.`, } ); -const txtChangeButton = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownForm.changeButton', - { - defaultMessage: 'Change', - } -); +const txtChangeButton = i18n.translate('uiActionsEnhanced.components.DrilldownForm.changeButton', { + defaultMessage: 'Change', +}); const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/index.ts similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/index.ts index 25aabcf7e6d2f..371b39b02aa60 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './action_factory'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/button_submit.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/button_submit.tsx similarity index 76% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/button_submit.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/button_submit.tsx index bf7dfcadd5ef7..6902c7f222a1f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/button_submit.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/button_submit.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/index.ts similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/index.ts index 770c32648591d..a78d7bd224efd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/button_submit/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './button_submit'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.stories.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.stories.tsx similarity index 89% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.stories.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.stories.tsx index 634436ed40185..cab21f147eb09 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.stories.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.stories.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.tsx similarity index 84% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.tsx index e923a7ed0629a..33032e7914e19 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/drilldown_form.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; @@ -11,20 +12,20 @@ import { i18n } from '@kbn/i18n'; import { TriggerPicker, TriggerPickerProps } from '../trigger_picker'; const txtNameOfDrilldown = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown', + 'uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown', { defaultMessage: 'Name', } ); const txtUntitledDrilldown = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownForm.untitledDrilldown', + 'uiActionsEnhanced.components.DrilldownForm.untitledDrilldown', { defaultMessage: 'Untitled drilldown', } ); -const txtTrigger = i18n.translate('xpack.uiActionsEnhanced.components.DrilldownForm.trigger', { +const txtTrigger = i18n.translate('uiActionsEnhanced.components.DrilldownForm.trigger', { defaultMessage: 'Trigger', }); diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/index.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/index.tsx new file mode 100644 index 0000000000000..08cbd42f32d9c --- /dev/null +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +export * from './drilldown_form'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.stories.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.stories.tsx similarity index 75% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.stories.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.stories.tsx index 87a32e7198e53..5a708a643b102 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.stories.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.stories.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.tsx similarity index 89% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.tsx index af9c78c9a5484..ac824c0f1fb33 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/drilldown_hello_bar.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/i18n.ts similarity index 59% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/i18n.ts index f7ba56213c246..6fbe7aed4c265 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/i18n.ts @@ -1,14 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtHelpText = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText', + 'uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText', { defaultMessage: 'Drilldowns enable you to define new behaviors for interacting with panels. You can add multiple actions and override the default filter.', @@ -16,14 +17,14 @@ export const txtHelpText = i18n.translate( ); export const txtViewDocsLinkLabel = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel', + 'uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel', { defaultMessage: 'View docs', } ); export const txtHideHelpButtonLabel = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel', + 'uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel', { defaultMessage: 'Hide', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/index.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/index.tsx similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/index.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/index.tsx index c0e7feabfc242..57aa8ea86e7bd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/index.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_hello_bar/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_hello_bar'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.stories.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.stories.tsx similarity index 92% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.stories.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.stories.tsx index 57e2e04807a6a..beeab600406db 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.stories.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.stories.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx similarity index 92% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx index 3576d7e34fd0b..854d918a6c16d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.tsx similarity index 96% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.tsx index 2cf4745cbbec4..3607ce42a69f5 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/drilldown_table.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/i18n.ts similarity index 54% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/i18n.ts index d465167555c31..14db707c84f70 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/i18n.ts @@ -1,35 +1,36 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { i18n } from '@kbn/i18n'; export const txtCreateDrilldown = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel', + 'uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel', { defaultMessage: 'Create new', } ); export const txtEditDrilldown = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel', + 'uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel', { defaultMessage: 'Edit', } ); export const txtCloneDrilldown = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel', + 'uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel', { defaultMessage: 'Copy', } ); export const txtDeleteDrilldowns = (count: number) => - i18n.translate('xpack.uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel', { + i18n.translate('uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel', { defaultMessage: 'Delete ({count})', values: { count, @@ -37,28 +38,28 @@ export const txtDeleteDrilldowns = (count: number) => }); export const txtSelectDrilldown = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel', + 'uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel', { defaultMessage: 'Select this drilldown', } ); export const txtName = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTable.nameColumnTitle', + 'uiActionsEnhanced.components.DrilldownTable.nameColumnTitle', { defaultMessage: 'Name', } ); export const txtAction = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTable.actionColumnTitle', + 'uiActionsEnhanced.components.DrilldownTable.actionColumnTitle', { defaultMessage: 'Action', } ); export const txtTrigger = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle', + 'uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle', { defaultMessage: 'Trigger', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/index.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/index.tsx similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/index.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/index.tsx index 1600cd63f4768..a8e7148de3352 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/index.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_table/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_table'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/drilldown_template_table.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/drilldown_template_table.tsx similarity index 94% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/drilldown_template_table.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/drilldown_template_table.tsx index 85d629dae9a1b..feabf1a75556b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/drilldown_template_table.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/drilldown_template_table.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React, { useState } from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/i18n.ts similarity index 63% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/i18n.ts index cfdc5b99f338d..f76b0d54dff87 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/i18n.ts @@ -1,21 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtSelectableMessage = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage', + 'uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage', { defaultMessage: 'Select this template', } ); export const txtNameColumnTitle = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle', + 'uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle', { defaultMessage: 'Name', description: 'Title of the first column in drilldown template cloning table.', @@ -23,7 +24,7 @@ export const txtNameColumnTitle = i18n.translate( ); export const txtSourceColumnTitle = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle', + 'uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle', { defaultMessage: 'Panel', description: 'Column title which describes from where the drilldown is cloned.', @@ -31,21 +32,21 @@ export const txtSourceColumnTitle = i18n.translate( ); export const txtActionColumnTitle = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle', + 'uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle', { defaultMessage: 'Action', } ); export const txtTriggerColumnTitle = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle', + 'uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle', { defaultMessage: 'Trigger', } ); export const txtSingleItemCopyActionLabel = i18n.translate( - 'xpack.uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction', + 'uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction', { defaultMessage: 'Copy', description: '"Copy" action button label in drilldown template cloning table last column.', @@ -53,7 +54,7 @@ export const txtSingleItemCopyActionLabel = i18n.translate( ); export const txtCopyButtonLabel = (count: number) => - i18n.translate('xpack.uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel', { + i18n.translate('uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel', { defaultMessage: 'Copy ({count})', description: 'Label of drilldown template table bottom copy button.', values: { diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/index.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/index.tsx similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/index.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/index.tsx index 0753e03a44e6f..6d356114d72cc 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/index.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_template_table/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_template_table'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.stories.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.stories.tsx similarity index 87% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.stories.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.stories.tsx index 9c4d98ebdf44e..9ec3847b3af9f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.stories.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.stories.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ /* eslint-disable no-console */ diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx similarity index 93% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx index f3cfe38910588..eeaa62e0e5964 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.tsx similarity index 92% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.tsx index f953776953067..6cf6420674868 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/i18n.ts similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/i18n.ts index e13d4a1d579dc..c96563b20aa90 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/i18n.ts @@ -1,21 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtClose = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel', + 'uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel', { defaultMessage: 'Close', } ); export const txtBack = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel', + 'uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel', { defaultMessage: 'Back', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/index.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/index.tsx similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/index.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/index.tsx index 4046df235f8f0..b116055da694e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/index.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './flyout_frame'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/index.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/index.tsx similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/index.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/index.tsx index bf312643597f0..5242e6e0679af 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/index.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './text_with_icon'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/text_with_icon.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/text_with_icon.tsx similarity index 88% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/text_with_icon.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/text_with_icon.tsx index 9b7fe346547b1..03cf65db41567 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/text_with_icon.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/text_with_icon/text_with_icon.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/index.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/index.tsx similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/index.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/index.tsx index 12a55d19d8fa1..d0e4f35f3b4e6 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/index.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './trigger_line_item'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/trigger_line_item.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/trigger_line_item.tsx similarity index 78% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/trigger_line_item.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/trigger_line_item.tsx index c8f4ccdd2fbf3..3fb19bc863ce2 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/trigger_line_item.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_line_item/trigger_line_item.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; @@ -10,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { TextWithIcon } from '../text_with_icon'; export const txtIncompatibleTooltip = i18n.translate( - 'xpack.uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip', + 'uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip', { defaultMessage: 'This trigger type not supported by this panel', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/index.ts similarity index 59% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/index.ts index 4a30a0494e8f7..2a1b80ad1f268 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export type { TriggerPickerItemDescription } from './trigger_picker_item'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.stories.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.stories.tsx similarity index 92% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.stories.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.stories.tsx index 77f3eadcc0bca..dc4e9c421f46f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.stories.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.stories.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.tsx similarity index 84% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.tsx index 728b144b4db0e..700e621c36226 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; @@ -11,21 +12,21 @@ import { i18n } from '@kbn/i18n'; import { TriggerPickerItemDescription, TriggerPickerItem } from './trigger_picker_item'; const txtTriggerPickerLabel = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel', + 'uiActionsEnhanced.components.actionWizard.triggerPickerLabel', { defaultMessage: 'Show option on:', } ); const txtTriggerPickerHelpText = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText', + 'uiActionsEnhanced.components.actionWizard.triggerPickerHelpText', { defaultMessage: "What's this?", } ); const txtTriggerPickerHelpTooltip = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip', + 'uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip', { defaultMessage: 'Determines when the drilldown appears in context menu', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker_item.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker_item.tsx similarity index 84% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker_item.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker_item.tsx index 70ab08d20f0c1..f6aaf7dda3f62 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker_item.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/trigger_picker/trigger_picker_item.tsx @@ -1,15 +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. + * 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 React from 'react'; import { EuiSpacer, EuiText, EuiCheckableCard, EuiTextColor, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -const txtUnknown = i18n.translate('xpack.uiActionsEnhanced.components.TriggerPickerItem.unknown', { +const txtUnknown = i18n.translate('uiActionsEnhanced.components.TriggerPickerItem.unknown', { defaultMessage: 'Unknown', }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/types.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/types.ts similarity index 54% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/types.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/types.ts index 052211d523896..2f14737407580 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/types.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/types.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export type { ActionFactoryPlaceContext } from '../types'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx similarity index 86% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx index f52ac6e161577..472149fbd2889 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/index.ts new file mode 100644 index 0000000000000..47e9321e74814 --- /dev/null +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +export * from './action_factory_picker'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/action_factory_view.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/action_factory_view.tsx similarity index 85% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/action_factory_view.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/action_factory_view.tsx index 0a389d6f1f615..a0ee33d54140f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/action_factory_view.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/action_factory_view.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/index.ts index d56fb06510fff..9ac4144c6c850 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_view/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './action_factory_view'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/context.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/context.tsx similarity index 81% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/context.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/context.tsx index cc0434b86bdcb..e8249d194b089 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/context.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/context.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/index.ts similarity index 50% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/index.ts index e9a2f3ab7be99..d08ff5608178d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/context/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './context'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/create_drilldown_form.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/create_drilldown_form.tsx similarity index 85% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/create_drilldown_form.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/create_drilldown_form.tsx index bd4a4fd3d051b..4551c7674ccd7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/create_drilldown_form.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/create_drilldown_form.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; @@ -16,7 +17,7 @@ import { DrilldownStateForm } from '../drilldown_state_form'; import { ButtonSubmit } from '../../components/button_submit'; const txtCreateDrilldown = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title', + 'uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title', { defaultMessage: 'Create Drilldown', description: 'Drilldowns flyout title for new drilldown form.', @@ -24,7 +25,7 @@ const txtCreateDrilldown = i18n.translate( ); const txtCreateDrilldownButton = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton', + 'uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton', { defaultMessage: 'Create drilldown', description: 'Primary button on new drilldown creation form.', diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/index.ts index 0f5fcbca00fe0..07fa0428da5eb 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/create_drilldown_form/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './create_drilldown_form'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/cloning_notification.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/cloning_notification.tsx similarity index 62% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/cloning_notification.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/cloning_notification.tsx index 3c292fcb49ff3..794f0f85a751c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/cloning_notification.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/cloning_notification.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { EuiCallOut, EuiSpacer, EuiLink } from '@elastic/eui'; @@ -10,7 +11,7 @@ import * as React from 'react'; import { i18n } from '@kbn/i18n'; const txtDismiss = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss', + 'uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss', { defaultMessage: 'Dismiss', description: 'Dismiss button in cloning notification callout.', @@ -18,16 +19,13 @@ const txtDismiss = i18n.translate( ); const txtBody = (count: number) => - i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body', - { - defaultMessage: '{count, number} {count, plural, one {drilldown} other {drilldowns}} copied.', - description: 'Title of notification show when one or more drilldowns were copied.', - values: { - count, - }, - } - ); + i18n.translate('uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body', { + defaultMessage: '{count, number} {count, plural, one {drilldown} other {drilldowns}} copied.', + description: 'Title of notification show when one or more drilldowns were copied.', + values: { + count, + }, + }); export interface CloningNotificationProps { count?: number; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/drilldown_list.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/drilldown_list.tsx similarity index 87% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/drilldown_list.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/drilldown_list.tsx index 41f062ae87327..abe73165ddcd4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/drilldown_list.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/drilldown_list.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/index.ts similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/index.ts index 40e01173d1cca..8e2d85c213c91 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_list/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_list'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/create_public_drilldown_manager.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/create_public_drilldown_manager.tsx similarity index 89% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/create_public_drilldown_manager.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/create_public_drilldown_manager.tsx index 23611397b3e2d..9e29173190238 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/create_public_drilldown_manager.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/create_public_drilldown_manager.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager.tsx similarity index 85% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager.tsx index a09411a14d83f..ca9224a1cd11e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_content.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_content.tsx similarity index 79% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_content.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_content.tsx index a0a7d94080ea0..5d6acc72b5032 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_content.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_content.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_with_provider.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_with_provider.tsx similarity index 74% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_with_provider.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_with_provider.tsx index 6f67a91f3feaa..c17d907cc4afc 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_with_provider.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/drilldown_manager_with_provider.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/index.ts similarity index 53% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/index.ts index 82cb861d496b9..c9bebf443cfea 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './create_public_drilldown_manager'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/drilldown_manager_footer.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/drilldown_manager_footer.tsx similarity index 78% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/drilldown_manager_footer.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/drilldown_manager_footer.tsx index 5cd5c712a1493..be660c6989637 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/drilldown_manager_footer.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/drilldown_manager_footer.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/index.ts index 61e6d642515c8..5c0c9a68215ac 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_footer/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_manager_footer'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/drilldown_manager_title.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/drilldown_manager_title.tsx similarity index 78% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/drilldown_manager_title.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/drilldown_manager_title.tsx index 63a02f65df939..18eac9bdfed1c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/drilldown_manager_title.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/drilldown_manager_title.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/index.ts index 8e015bd7bca06..e6fd47f81ab84 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_manager_title/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_manager_title'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/drilldown_state_form.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/drilldown_state_form.tsx similarity index 88% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/drilldown_state_form.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/drilldown_state_form.tsx index 44b9cf60916fb..33a5a7e6c016e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/drilldown_state_form.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/drilldown_state_form.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/index.ts index e06cb66ff99bc..c20a3f0034c8c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/drilldown_state_form/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_state_form'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/edit_drilldown_form.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/edit_drilldown_form.tsx similarity index 86% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/edit_drilldown_form.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/edit_drilldown_form.tsx index 09b64260854d6..a7411f40a79f9 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/edit_drilldown_form.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/edit_drilldown_form.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; @@ -16,7 +17,7 @@ import { DrilldownStateForm } from '../drilldown_state_form'; import { ButtonSubmit } from '../../components/button_submit'; const txtEditDrilldown = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title', + 'uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title', { defaultMessage: 'Edit Drilldown', description: 'Drilldowns flyout title for edit drilldown form.', @@ -24,7 +25,7 @@ const txtEditDrilldown = i18n.translate( ); const txtEditDrilldownButton = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton', + 'uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton', { defaultMessage: 'Save', description: 'Primary button on new drilldown edit form.', diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/index.ts index ec10df49a727d..9e49fd9cc0e43 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/edit_drilldown_form/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './edit_drilldown_form'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/create_drilldown_form.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/create_drilldown_form.tsx similarity index 87% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/create_drilldown_form.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/create_drilldown_form.tsx index 3382a5dd82fbb..ee0b045525f84 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/create_drilldown_form.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/create_drilldown_form.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/edit_drilldown_form.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/edit_drilldown_form.tsx similarity index 86% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/edit_drilldown_form.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/edit_drilldown_form.tsx index 1ee8291b30774..ee0bda6f79327 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/edit_drilldown_form.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/edit_drilldown_form.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; @@ -14,7 +15,7 @@ import { DrilldownState } from '../../state'; import { TriggerPickerProps } from '../../components/trigger_picker'; export const txtDeleteDrilldownButtonLabel = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel', { defaultMessage: 'Delete drilldown', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/form_drilldown_wizard.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/form_drilldown_wizard.tsx similarity index 81% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/form_drilldown_wizard.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/form_drilldown_wizard.tsx index ddb08ee407118..738d7bf25b309 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/i18n.ts similarity index 53% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/i18n.ts index 28ebe53e05f87..8b32fbc5c7b8d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/i18n.ts @@ -1,28 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtCreateDrilldownTitle = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle', { defaultMessage: 'Create Drilldown', } ); export const txtEditDrilldownTitle = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle', { defaultMessage: 'Edit Drilldown', } ); export const txtDeleteDrilldownButtonLabel = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel', { defaultMessage: 'Delete drilldown', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/index.ts similarity index 52% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/index.ts index fafb07bbc1575..59f0ae47682e2 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/form_drilldown_wizard/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './form_drilldown_wizard'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/hello_bar.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/hello_bar.tsx similarity index 77% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/hello_bar.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/hello_bar.tsx index 08fbcb4ad2421..0f42478aba007 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/hello_bar.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/hello_bar.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/index.ts similarity index 50% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/index.ts index 54c7f1a8b131b..bb4fa2060d256 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/hello_bar/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './hello_bar'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/index.ts similarity index 62% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/index.ts index cb1e6099c9b53..28d8c540579dd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export type { PublicDrilldownManagerComponent } from './drilldown_manager'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/index.ts similarity index 50% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/index.ts index dde0170c6f13b..5e8ce360289ab 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './tabs'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/tabs.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/tabs.tsx similarity index 80% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/tabs.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/tabs.tsx index c8514d91a343b..2e0c7a2252bb5 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/tabs.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/tabs/tabs.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; @@ -14,14 +15,14 @@ import { DrilldownList } from '../drilldown_list'; import { TemplatePicker } from '../template_picker'; export const txtCreateNew = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.containers.DrilldownManager.createNew', + 'uiActionsEnhanced.drilldowns.containers.DrilldownManager.createNew', { defaultMessage: 'Create new', } ); export const txtManage = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.containers.DrilldownManager.manage', + 'uiActionsEnhanced.drilldowns.containers.DrilldownManager.manage', { defaultMessage: 'Manage', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/i18n.ts similarity index 57% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/i18n.ts index e947e246081d5..11acd9dc135f3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/i18n.ts @@ -1,14 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtLabel = i18n.translate( - 'xpack.uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label', + 'uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label', { defaultMessage: 'Copy existing drilldown', description: 'Label above template picker table.', diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/index.ts similarity index 51% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/index.ts index 98b9cf637cf9e..3f9e5a78d0b19 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './template_picker'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_list.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_list.tsx similarity index 90% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_list.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_list.tsx index 85422370e88c2..3c837216f5e33 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_list.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_list.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { EuiTitle, EuiSpacer } from '@elastic/eui'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_picker.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_picker.tsx similarity index 73% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_picker.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_picker.tsx index d6004e71ec60a..1856f3a8f535d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_picker.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/template_picker/template_picker.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/hooks/use_sync_observable.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/hooks/use_sync_observable.ts similarity index 86% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/hooks/use_sync_observable.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/hooks/use_sync_observable.ts index d5c7bda0377f9..a1fa1430e419f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/hooks/use_sync_observable.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/hooks/use_sync_observable.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { useRef, useMemo } from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/index.ts similarity index 53% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/index.ts index b00fcf5f33959..d7a52ced7a586 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.test.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.test.tsx similarity index 97% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.test.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.test.tsx index 6e6cd30f65116..7addda418d354 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.test.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { ActionFactory, MemoryActionStorage } from '../../../dynamic_actions'; @@ -50,7 +51,7 @@ const createDrilldownManagerState = () => { execute: async () => {}, }), }, - {} + { getFeatureUsageStart: () => undefined } ); const factory2 = new ActionFactory( { @@ -64,7 +65,7 @@ const createDrilldownManagerState = () => { execute: async () => {}, }), }, - {} + { getFeatureUsageStart: () => undefined } ); const factory3 = new ActionFactory( { @@ -78,7 +79,7 @@ const createDrilldownManagerState = () => { execute: async () => {}, }), }, - {} + { getFeatureUsageStart: () => undefined } ); const trigger1: Trigger = { id: 'TRIGGER1', diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts similarity index 98% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts index 231057a50ee1f..2d563f20a583a 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 useObservable from 'react-use/lib/useObservable'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_state.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_state.ts similarity index 97% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_state.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_state.ts index d16a9a93930dd..6f90b1c3e1a09 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_state.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_state.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 useObservable from 'react-use/lib/useObservable'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/i18n.ts similarity index 64% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/i18n.ts index c9b49b5ff9346..380e480eccf70 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/i18n.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { i18n } from '@kbn/i18n'; @@ -10,7 +11,7 @@ import { i18n } from '@kbn/i18n'; export const toastDrilldownCreated = { title: (drilldownName: string) => i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle', { defaultMessage: 'Drilldown "{drilldownName}" created', values: { @@ -19,7 +20,7 @@ export const toastDrilldownCreated = { } ), text: i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText', { // TODO: remove `Save your dashboard before testing.` part // when drilldowns are used not only in dashboard @@ -32,7 +33,7 @@ export const toastDrilldownCreated = { export const toastDrilldownEdited = { title: (drilldownName: string) => i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle', { defaultMessage: 'Drilldown "{drilldownName}" updated', values: { @@ -41,7 +42,7 @@ export const toastDrilldownEdited = { } ), text: i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText', { defaultMessage: 'Save your dashboard before testing.', } @@ -50,13 +51,13 @@ export const toastDrilldownEdited = { export const toastDrilldownDeleted = { title: i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle', { defaultMessage: 'Drilldown deleted', } ), text: i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText', { defaultMessage: 'Save your dashboard before testing.', } @@ -66,14 +67,14 @@ export const toastDrilldownDeleted = { export const toastDrilldownsDeleted = { title: (n: number) => i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle', { defaultMessage: '{n} drilldowns deleted', values: { n }, } ), text: i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText', { defaultMessage: 'Save your dashboard before testing.', } @@ -81,7 +82,7 @@ export const toastDrilldownsDeleted = { }; export const toastDrilldownsCRUDError = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle', { defaultMessage: 'Error saving drilldown', description: 'Title for generic error toast when persisting drilldown updates failed', @@ -89,7 +90,7 @@ export const toastDrilldownsCRUDError = i18n.translate( ); export const insufficientLicenseLevel = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError', { defaultMessage: 'Insufficient license level', description: @@ -99,7 +100,7 @@ export const insufficientLicenseLevel = i18n.translate( export const invalidDrilldownType = (type: string) => i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType', + 'uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType', { defaultMessage: "Drilldown type {type} doesn't exist", values: { @@ -109,7 +110,7 @@ export const invalidDrilldownType = (type: string) => ); export const txtDefaultTitle = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.drilldownManager.state.defaultTitle', + 'uiActionsEnhanced.drilldowns.drilldownManager.state.defaultTitle', { defaultMessage: 'Drilldowns', description: 'Drilldowns flyout title.', diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/index.ts similarity index 56% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/index.ts index 42dcc4510622a..21f8c4961307b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_state'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts similarity index 96% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts index 59a2e5598fe87..84ca97036f1b4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { ToastsStart } from '@kbn/core/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/index.ts similarity index 56% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/index.ts index ebaa845409e2a..b0773c88e10a3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './drilldown_definition'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts similarity index 56% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts index ac2ddd1bfc14d..4e73e9a43a9e2 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export { UrlDrilldownCollectConfig } from './url_drilldown_collect_config'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts similarity index 60% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts index 505fbf4c36b5d..dcb36cbab257a 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts @@ -1,14 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtUrlTemplatePlaceholder = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText', { defaultMessage: 'Example: {exampleUrl}', values: { @@ -18,7 +19,7 @@ export const txtUrlTemplatePlaceholder = i18n.translate( ); export const txtUrlPreviewHelpText = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText', { defaultMessage: 'Please note that in preview \\{\\{event.*\\}\\} variables are substituted with dummy values.', @@ -26,56 +27,56 @@ export const txtUrlPreviewHelpText = i18n.translate( ); export const txtUrlTemplateLabel = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel', { defaultMessage: 'Enter URL', } ); export const txtUrlTemplateSyntaxHelpLinkText = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText', { defaultMessage: 'Syntax help', } ); export const txtUrlTemplatePreviewLabel = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel', { defaultMessage: 'URL preview:', } ); export const txtUrlTemplatePreviewLinkText = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText', { defaultMessage: 'Preview', } ); export const txtUrlTemplateOpenInNewTab = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel', { defaultMessage: 'Open in new window', } ); export const txtUrlTemplateAdditionalOptions = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions', { defaultMessage: 'Additional options', } ); export const txtUrlTemplateEncodeUrl = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl', { defaultMessage: 'Encode URL', } ); export const txtUrlTemplateEncodeDescription = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription', { defaultMessage: 'If enabled, URL will be escaped using percent encoding', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss similarity index 100% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts similarity index 59% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts index 5aa1dddd64f8c..fd96f908fda23 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export type { UrlDrilldownCollectConfigProps } from './lazy'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx similarity index 79% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx index d4b04ce489dab..eb666d6151ece 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 * as React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx similarity index 81% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx index 805b9954754fd..8fc2fe3c68c2e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx similarity index 65% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx index 797a280e13d63..46089a8c51a94 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx similarity index 95% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx index 6556a8146a135..0e4825dd58e50 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React, { useRef } from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/i18n.ts similarity index 53% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/i18n.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/i18n.ts index ece7a71778eb9..fb46cbd4ef700 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/i18n.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/i18n.ts @@ -1,28 +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. + * 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 { i18n } from '@kbn/i18n'; export const txtAddVariableButtonTitle = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle', { defaultMessage: 'Add variable', } ); export const txtUrlTemplateVariablesHelpLinkText = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText', { defaultMessage: 'Help', } ); export const txtUrlTemplateVariablesFilterPlaceholderText = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText', + 'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText', { defaultMessage: 'Filter variables', } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/index.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/index.tsx similarity index 93% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/index.tsx rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/index.tsx index 963ed7aeb2484..bd18a1e037d1f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/index.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/variable_popover/index.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 React, { useState } from 'react'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts similarity index 96% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts index b0e86ecf71f7b..62b8c01c72880 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/handlebars.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 Handlebars from '@kbn/handlebars'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts similarity index 76% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts index db76a54cc8d5b..543546132a3a2 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export type { UrlDrilldownConfig, UrlDrilldownGlobalScope, UrlDrilldownScope } from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts similarity index 84% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts index 4d3ee2e32e8d7..3566b6712c78d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export type UrlDrilldownConfig = { diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts similarity index 73% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts index da584f68edfaa..69a78691eafe5 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { CoreSetup } from '@kbn/core/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts similarity index 98% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts index 032a54364b940..fefbe03327956 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { compile } from './url_template'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts similarity index 77% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts index 776ae70cab4a6..f26b40fc7f148 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export async function compile( diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts similarity index 94% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts index 78379b3495919..e73afe056c289 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { validateUrl, validateUrlTemplate } from './url_validation'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts similarity index 76% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts rename to src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts index 860e6f96cc782..7f1fb02ed9752 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { i18n } from '@kbn/i18n'; @@ -10,7 +11,7 @@ import { UrlDrilldownConfig, UrlDrilldownScope } from './types'; import { compile } from './url_template'; const generalFormatError = i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage', + 'uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage', { defaultMessage: 'Invalid format. Example: {exampleUrl}', values: { @@ -20,15 +21,12 @@ const generalFormatError = i18n.translate( ); const formatError = (message: string) => - i18n.translate( - 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage', - { - defaultMessage: 'Invalid format: {message}', - values: { - message, - }, - } - ); + i18n.translate('uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage', { + defaultMessage: 'Invalid format: {message}', + values: { + message, + }, + }); const SAFE_URL_PATTERN = /^(?:(?:https?|mailto):|[^&:/?#]*(?:[/?#]|$))/gi; export function validateUrl(url: string): { isValid: boolean; error?: string } { diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts similarity index 95% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts index 9d03e00514e24..d8a6d1f97ba0b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { ActionFactory } from './action_factory'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts similarity index 92% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 436c2e7bf8e8a..525362b809e9d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { UiComponent, CollectConfigProps } from '@kbn/kibana-utils-plugin/public'; @@ -30,7 +31,7 @@ import type { ActionFactoryDefinition } from './action_factory_definition'; export interface ActionFactoryDeps { readonly getLicense?: () => ILicense; - readonly getFeatureUsageStart?: () => LicensingPluginStart['featureUsage']; + readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage'] | undefined; } export class ActionFactory< @@ -133,15 +134,15 @@ export class ActionFactory< private notifyFeatureUsage(): void { if (!this.minimalLicense || !this.licenseFeatureName || !this.deps.getFeatureUsageStart) return; - this.deps - .getFeatureUsageStart() - .notifyUsage(this.licenseFeatureName) - .catch(() => { + const featureUsageStart = this.deps.getFeatureUsageStart(); + if (featureUsageStart) { + featureUsageStart.notifyUsage(this.licenseFeatureName).catch(() => { // eslint-disable-next-line no-console console.warn( `ActionFactory [actionFactory.id = ${this.def.id}] fail notify feature usage.` ); }); + } } public telemetry( diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts similarity index 91% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts index 029933e3b28d3..a509017440995 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { Configurable } from '@kbn/kibana-utils-plugin/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts similarity index 86% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts index ad59d8929b265..4a06b29c7a3f6 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { SerializableRecord } from '@kbn/utility-types'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts similarity index 71% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts index 8e94fead9a7d7..ad8af597cbc29 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { i18n } from '@kbn/i18n'; @@ -15,7 +16,7 @@ export const dynamicActionGrouping: PresentableGrouping<{ { id: 'dynamicActions', getDisplayName: () => - i18n.translate('xpack.uiActionsEnhanced.CustomActions', { + i18n.translate('uiActionsEnhanced.CustomActions', { defaultMessage: 'Custom actions', }), getIconType: () => 'symlink', diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts similarity index 99% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index ab7d322c8576e..ae75f95664152 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { DynamicActionManager } from './dynamic_action_manager'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts similarity index 98% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index a2bbc0db953c5..095cda269c49e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { v4 as uuidv4 } from 'uuid'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts similarity index 93% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts index 53afebb7490c0..7899816af7780 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { SerializedEvent } from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_storage.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_storage.ts similarity index 93% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_storage.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_storage.ts index 26e14818a12b5..d335378407e87 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_storage.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_storage.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ /* eslint-disable max-classes-per-file */ diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/index.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/index.ts similarity index 68% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/index.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/index.ts index 11ee33bff10de..c5721ed452931 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/index.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts similarity index 72% rename from x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts rename to src/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts index 2512753c07f4a..718909b148b6a 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { SerializedAction, SerializedEvent, BaseActionConfig } from '../../common/types'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/src/plugins/ui_actions_enhanced/public/index.ts similarity index 91% rename from x-pack/plugins/ui_actions_enhanced/public/index.ts rename to src/plugins/ui_actions_enhanced/public/index.ts index 0b327cd65ff09..b609c5807a562 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/index.ts +++ b/src/plugins/ui_actions_enhanced/public/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { PluginInitializerContext } from '@kbn/core/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/src/plugins/ui_actions_enhanced/public/mocks.ts similarity index 93% rename from x-pack/plugins/ui_actions_enhanced/public/mocks.ts rename to src/plugins/ui_actions_enhanced/public/mocks.ts index 5743d3d690500..356825e03e9d8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts +++ b/src/plugins/ui_actions_enhanced/public/mocks.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { CoreSetup, CoreStart } from '@kbn/core/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/src/plugins/ui_actions_enhanced/public/plugin.ts similarity index 91% rename from x-pack/plugins/ui_actions_enhanced/public/plugin.ts rename to src/plugins/ui_actions_enhanced/public/plugin.ts index b64f836ad9596..1065b30c8e4dc 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/src/plugins/ui_actions_enhanced/public/plugin.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { BehaviorSubject, Subscription } from 'rxjs'; @@ -28,13 +29,13 @@ import { dynamicActionEnhancement } from './dynamic_actions/dynamic_action_enhan interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: UiActionsSetup; - licensing: LicensingPluginSetup; + licensing?: LicensingPluginSetup; } export interface StartDependencies { embeddable: EmbeddableStart; uiActions: UiActionsStart; - licensing: LicensingPluginStart; + licensing?: LicensingPluginStart; } export interface SetupContract @@ -79,8 +80,8 @@ export class AdvancedUiActionsPublicPlugin const startServices = createStartServicesGetter(core.getStartServices); this.enhancements = new UiActionsServiceEnhancements({ getLicense: () => this.getLicenseInfo(), - featureUsageSetup: licensing.featureUsage, - getFeatureUsageStart: () => startServices().plugins.licensing.featureUsage, + featureUsageSetup: licensing?.featureUsage, + getFeatureUsageStart: () => startServices().plugins.licensing?.featureUsage, }); embeddable.registerEnhancement(dynamicActionEnhancement(this.enhancements)); return { @@ -90,7 +91,7 @@ export class AdvancedUiActionsPublicPlugin } public start(core: CoreStart, { uiActions, licensing }: StartDependencies): StartContract { - this.subs.push(licensing.license$.subscribe(this.licenseInfo)); + if (licensing) this.subs.push(licensing.license$.subscribe(this.licenseInfo)); const dateFormat = core.uiSettings.get('dateFormat') as string; const commonlyUsedRanges = core.uiSettings.get( diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/index.ts b/src/plugins/ui_actions_enhanced/public/services/index.ts similarity index 53% rename from x-pack/plugins/ui_actions_enhanced/public/services/index.ts rename to src/plugins/ui_actions_enhanced/public/services/index.ts index 59bb5e2d42a4d..05f6764d481a4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/index.ts +++ b/src/plugins/ui_actions_enhanced/public/services/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export * from './ui_actions_service_enhancements'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/src/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts similarity index 96% rename from x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts rename to src/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts index 28e7aaa774a7b..4548d1ada04e8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts +++ b/src/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { @@ -158,7 +159,7 @@ describe('UiActionsService', () => { }); describe('registerFeature for licensing', () => { - const spy = jest.spyOn(deps.featureUsageSetup, 'register'); + const spy = jest.spyOn(deps.featureUsageSetup!, 'register'); beforeEach(() => { spy.mockClear(); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/src/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts similarity index 92% rename from x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts rename to src/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index fb2dc3ea5bd03..9de325e281357 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/src/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { SerializableRecord } from '@kbn/utility-types'; @@ -26,9 +27,9 @@ export type { DynamicActionsState }; export interface UiActionsServiceEnhancementsParams { readonly actionFactories?: ActionFactoryRegistry; - readonly getLicense: () => ILicense; - readonly featureUsageSetup: LicensingPluginSetup['featureUsage']; - readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage']; + readonly getLicense?: () => ILicense; + readonly featureUsageSetup?: LicensingPluginSetup['featureUsage']; + readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage'] | undefined; } export class UiActionsServiceEnhancements @@ -155,7 +156,12 @@ export class UiActionsServiceEnhancements private registerFeatureUsage = (definition: ActionFactoryDefinition): void => { if (!definition.minimalLicense || !definition.licenseFeatureName) return; - this.deps.featureUsageSetup.register(definition.licenseFeatureName, definition.minimalLicense); + if (this.deps.featureUsageSetup) { + this.deps.featureUsageSetup.register( + definition.licenseFeatureName, + definition.minimalLicense + ); + } }; public readonly telemetry = ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/index.ts b/src/plugins/ui_actions_enhanced/public/test_helpers/index.ts similarity index 62% rename from x-pack/plugins/ui_actions_enhanced/public/test_helpers/index.ts rename to src/plugins/ui_actions_enhanced/public/test_helpers/index.ts index 497d4fe0ccdf0..26220094e4981 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/index.ts +++ b/src/plugins/ui_actions_enhanced/public/test_helpers/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ export { TimeRangeEmbeddable, TIME_RANGE_EMBEDDABLE } from './time_range_embeddable'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts b/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts similarity index 87% rename from x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts rename to src/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts index b37a0e54d75bd..f04f89e19e039 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts +++ b/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { diff --git a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts b/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts similarity index 80% rename from x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts rename to src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts index a916fe6601eed..b650bfc33ebae 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts +++ b/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { diff --git a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable_factory.ts b/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable_factory.ts similarity index 83% rename from x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable_factory.ts rename to src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable_factory.ts index 4f1fb3af965e8..ce04ee022c684 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable_factory.ts +++ b/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable_factory.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { diff --git a/x-pack/plugins/ui_actions_enhanced/public/types.ts b/src/plugins/ui_actions_enhanced/public/types.ts similarity index 72% rename from x-pack/plugins/ui_actions_enhanced/public/types.ts rename to src/plugins/ui_actions_enhanced/public/types.ts index 7541212569f55..0e0d8d70d5d4e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/types.ts +++ b/src/plugins/ui_actions_enhanced/public/types.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { KibanaReactOverlays } from '@kbn/kibana-react-plugin/public'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/src/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts similarity index 92% rename from x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts rename to src/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index e30567e1231de..617aea5f05051 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/src/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { SerializableRecord } from '@kbn/utility-types'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/index.ts b/src/plugins/ui_actions_enhanced/server/index.ts similarity index 81% rename from x-pack/plugins/ui_actions_enhanced/server/index.ts rename to src/plugins/ui_actions_enhanced/server/index.ts index a1d70d3ea4a83..04df6d4b1e841 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/index.ts +++ b/src/plugins/ui_actions_enhanced/server/index.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { AdvancedUiActionsServerPlugin } from './plugin'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/src/plugins/ui_actions_enhanced/server/plugin.ts similarity index 91% rename from x-pack/plugins/ui_actions_enhanced/server/plugin.ts rename to src/plugins/ui_actions_enhanced/server/plugin.ts index f021924db3731..272657ec84f0b 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/src/plugins/ui_actions_enhanced/server/plugin.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { identity } from 'lodash'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts b/src/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts similarity index 94% rename from x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts rename to src/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts index 63064e06184a6..1a2a30448b0cf 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts +++ b/src/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts b/src/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts similarity index 78% rename from x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts rename to src/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts index 68a314daa121a..7d55ce8ceaeea 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts +++ b/src/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { DynamicActionsState } from '../../common'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts b/src/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts similarity index 97% rename from x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts rename to src/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts index 67dc759d90f49..d8fca89205cab 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts +++ b/src/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { dynamicActionsCollector } from './dynamic_actions_collector'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts b/src/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts similarity index 86% rename from x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts rename to src/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts index 0f0f737e9d21f..dd059bc75af38 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts +++ b/src/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { DynamicActionsState } from '../../common'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts b/src/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts similarity index 64% rename from x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts rename to src/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts index 78678e22db63f..ffc5982585a43 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts +++ b/src/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ const prefix = 'dynamicActions.'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/types.ts b/src/plugins/ui_actions_enhanced/server/types.ts similarity index 80% rename from x-pack/plugins/ui_actions_enhanced/server/types.ts rename to src/plugins/ui_actions_enhanced/server/types.ts index 7280846e9330d..be6570ef1fc97 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/types.ts +++ b/src/plugins/ui_actions_enhanced/server/types.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { PersistableState, PersistableStateDefinition } from '@kbn/kibana-utils-plugin/common'; diff --git a/src/plugins/ui_actions_enhanced/tsconfig.json b/src/plugins/ui_actions_enhanced/tsconfig.json new file mode 100644 index 0000000000000..d9d9474f0bd72 --- /dev/null +++ b/src/plugins/ui_actions_enhanced/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "common/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../embeddable/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../ui_actions/tsconfig.json" }, + { "path": "../../../x-pack/plugins/licensing/tsconfig.json" }, + ] +} diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts index 085ba3dc0979f..1c8f98afec470 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts @@ -13,6 +13,7 @@ import type { KueryNode } from '@kbn/es-query'; import { setupGetFieldSuggestions } from './field'; import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; import { coreMock } from '@kbn/core/public/mocks'; +import type { DataViewField } from '@kbn/data-views-plugin/public'; const mockKueryNode = (kueryNode: Partial) => kueryNode as unknown as KueryNode; @@ -39,7 +40,9 @@ describe('Kuery field suggestions', () => { querySuggestionsArgs, mockKueryNode({ prefix, suffix }) ); - const filterableFields = indexPatternResponse.fields.filter(indexPatternsUtils.isFilterable); + const filterableFields = (indexPatternResponse.fields as DataViewField[]).filter( + indexPatternsUtils.isFilterable + ); expect(suggestions.length).toBe(filterableFields.length); }); diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx index 37f9c4658b81a..1df09621aee91 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -// for replace IFieldType => DataViewField need to fix the issue https://github.com/elastic/kibana/issues/131292 -import { IFieldType, indexPatterns as indexPatternsUtils } from '@kbn/data-plugin/public'; +import { indexPatterns as indexPatternsUtils } from '@kbn/data-plugin/public'; +import type { DataViewField } from '@kbn/data-views-plugin/public'; import { flatten } from 'lodash'; import { sortPrefixFirst } from './sort_prefix_first'; import { QuerySuggestionField, QuerySuggestionTypes } from '../query_suggestion_provider'; import { KqlQuerySuggestionProvider } from './types'; -const keywordComparator = (first: IFieldType, second: IFieldType) => { +const keywordComparator = (first: DataViewField, second: DataViewField) => { const extensions = ['raw', 'keyword']; if (extensions.map((ext) => `${first.name}.${ext}`).includes(second.name)) { return 1; @@ -32,7 +32,8 @@ export const setupGetFieldSuggestions: KqlQuerySuggestionProvider { return indexPattern.fields.filter(indexPatternsUtils.isFilterable); }) - ); + // temp until IIndexPattern => DataView + ) as DataViewField[]; const search = `${prefix}${suffix}`.trim().toLowerCase(); const matchingFields = allFields.filter((field) => { const subTypeNested = indexPatternsUtils.getFieldSubtypeNested(field); diff --git a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts index 9eebc5f5bd435..57c4db1cbf1f7 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts @@ -8,10 +8,7 @@ import { flatten } from 'lodash'; import { CoreSetup } from '@kbn/core/public'; -// for replace IIndexPattern => DataView and IFieldType => DataViewField -// need to fix the issue https://github.com/elastic/kibana/issues/131292 -import type { IFieldType } from '@kbn/data-views-plugin/common'; -import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { escapeQuotes } from './lib/escape_kuery'; import { KqlQuerySuggestionProvider } from './types'; import type { UnifiedSearchPublicPluginStart } from '../../../types'; @@ -39,7 +36,7 @@ export const setupGetValueSuggestions: KqlQuerySuggestionProvider = ( ): Promise => { const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName; - const indexPatternFieldEntries: Array<[DataView, IFieldType]> = []; + const indexPatternFieldEntries: Array<[DataView, DataViewField]> = []; indexPatterns.forEach((indexPattern) => { indexPattern.fields .filter((field) => field.name === fullFieldName) diff --git a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts index d5b42fec2e842..310e3d402df34 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -12,6 +12,7 @@ import type { TimefilterSetup } from '@kbn/data-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { setupValueSuggestionProvider } from './value_suggestion_provider'; import type { ValueSuggestionsGetFn } from './value_suggestion_provider'; +import type { DataView } from '@kbn/data-views-plugin/public'; describe('FieldSuggestions', () => { let getValueSuggestions: ValueSuggestionsGetFn; @@ -186,7 +187,7 @@ describe('FieldSuggestions', () => { ...stubIndexPattern, title: 'customIndexPattern', useTimeRange: false, - } as any; + } as unknown as DataView; await getValueSuggestions({ indexPattern: customIndexPattern, @@ -195,7 +196,7 @@ describe('FieldSuggestions', () => { useTimeRange: false, }); await getValueSuggestions({ - indexPattern: customIndexPattern, + indexPattern: customIndexPattern as unknown as DataView, field: fields[0], query: 'query', useTimeRange: false, diff --git a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts index 92b85536df7d5..f935cd9362b56 100644 --- a/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts @@ -10,18 +10,15 @@ import { CoreSetup } from '@kbn/core/public'; import dateMath from '@kbn/datemath'; import { memoize } from 'lodash'; import { UI_SETTINGS, ValueSuggestionsMethod } from '@kbn/data-plugin/common'; -// for replace IIndexPattern => DataView and IFieldType => DataViewField -// need to fix the issue https://github.com/elastic/kibana/issues/131292 -import type { IFieldType } from '@kbn/data-views-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { TimefilterSetup } from '@kbn/data-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/common'; import { AutocompleteUsageCollector } from '../collectors'; export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise; interface ValueSuggestionsGetFnArgs { indexPattern: DataView; - field: IFieldType; + field: DataViewField; query: string; useTimeRange?: boolean; boolFilter?: any[]; @@ -49,7 +46,7 @@ export const setupValueSuggestionProvider = ( usageCollector, }: { timefilter: TimefilterSetup; usageCollector?: AutocompleteUsageCollector } ): ValueSuggestionsGetFn => { - function resolver(title: string, field: IFieldType, query: string, filters: any[]) { + function resolver(title: string, field: DataViewField, query: string, filters: any[]) { // Only cache results for a minute const ttl = Math.floor(Date.now() / 1000 / 60); return [ttl, query, title, field.name, JSON.stringify(filters)].join('|'); @@ -58,7 +55,7 @@ export const setupValueSuggestionProvider = ( const requestSuggestions = memoize( ( index: string, - field: IFieldType, + field: DataViewField, query: string, filters: any = [], signal?: AbortSignal, diff --git a/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts index 03ceffc73b34f..b259e52707426 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts @@ -9,7 +9,7 @@ import { coreMock } from '@kbn/core/server/mocks'; import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { ConfigSchema } from '../../config'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { DataViewField } from '@kbn/data-views-plugin/common'; import { termsAggSuggestions } from './terms_agg'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; diff --git a/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts b/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts index f0209e66ee58d..a3de47ae45a72 100644 --- a/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts @@ -10,7 +10,7 @@ import { termsEnumSuggestions } from './terms_enum'; import { coreMock } from '@kbn/core/server/mocks'; import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { ConfigSchema } from '../../config'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { TermsEnumResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { DataViewField } from '@kbn/data-views-plugin/common'; diff --git a/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap index 73c0ee3e38d7f..79af22ed442a7 100644 --- a/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/gauge/public/__snapshots__/to_ast.test.ts.snap @@ -3,35 +3,6 @@ exports[`gauge vis toExpressionAst function with minimal params 1`] = ` Object { "chain": Array [ - Object { - "arguments": Object { - "aggs": Array [], - "index": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "id": Array [ - "123", - ], - }, - "function": "indexPatternLoad", - "type": "function", - }, - ], - "type": "expression", - }, - ], - "metricsAtAllLevels": Array [ - false, - ], - "partialRows": Array [ - false, - ], - }, - "function": "esaggs", - "type": "function", - }, Object { "arguments": Object { "centralMajorMode": Array [ diff --git a/src/plugins/vis_types/gauge/public/to_ast.test.ts b/src/plugins/vis_types/gauge/public/to_ast.test.ts index f3b8ee90b5b55..f88743dd70b2c 100644 --- a/src/plugins/vis_types/gauge/public/to_ast.test.ts +++ b/src/plugins/vis_types/gauge/public/to_ast.test.ts @@ -34,13 +34,7 @@ describe('gauge vis toExpressionAst function', () => { }, }, }, - data: { - indexPattern: { id: '123' } as any, - aggs: { - getResponseAggs: () => [], - aggs: [], - } as any, - }, + data: {}, } as unknown as Vis; }); diff --git a/src/plugins/vis_types/gauge/public/to_ast.ts b/src/plugins/vis_types/gauge/public/to_ast.ts index 85148b713b319..697b9790468a3 100644 --- a/src/plugins/vis_types/gauge/public/to_ast.ts +++ b/src/plugins/vis_types/gauge/public/to_ast.ts @@ -14,7 +14,6 @@ import type { } from '@kbn/expression-gauge-plugin/common'; import { GaugeType, GaugeVisParams } from './types'; import { getStopsWithColorsFromRanges } from './utils'; -import { getEsaggsFn } from './to_ast_esaggs'; const prepareDimension = (params: SchemaConfig) => { const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor }); @@ -90,7 +89,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, params) gauge.addArgument('palette', buildExpression([palette])); } - const ast = buildExpression([getEsaggsFn(vis), gauge]); + const ast = buildExpression([gauge]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/gauge/public/to_ast_esaggs.ts b/src/plugins/vis_types/gauge/public/to_ast_esaggs.ts deleted file mode 100644 index 4b098342c6de2..0000000000000 --- a/src/plugins/vis_types/gauge/public/to_ast_esaggs.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { Vis } from '@kbn/visualizations-plugin/public'; -import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '@kbn/data-plugin/public'; - -import { GaugeVisParams } from './types'; - -/** - * Get esaggs expressions function - * @param vis - */ -export function getEsaggsFn(vis: Vis) { - return buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); -} diff --git a/src/plugins/vis_types/gauge/public/vis_type/gauge.tsx b/src/plugins/vis_types/gauge/public/vis_type/gauge.tsx index 3de528bfe5e6c..b6bdf93a8ea89 100644 --- a/src/plugins/vis_types/gauge/public/vis_type/gauge.tsx +++ b/src/plugins/vis_types/gauge/public/vis_type/gauge.tsx @@ -29,6 +29,7 @@ export const getGaugeVisTypeDefinition = ( defaultMessage: 'Show the status of a metric.', }), getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], + fetchDatatable: true, toExpressionAst, visConfig: { defaults: { diff --git a/src/plugins/vis_types/gauge/public/vis_type/goal.tsx b/src/plugins/vis_types/gauge/public/vis_type/goal.tsx index c953cd3e5dfe2..bcf7596094cd1 100644 --- a/src/plugins/vis_types/gauge/public/vis_type/goal.tsx +++ b/src/plugins/vis_types/gauge/public/vis_type/goal.tsx @@ -27,6 +27,7 @@ export const getGoalVisTypeDefinition = ( description: i18n.translate('visTypeGauge.goal.goalDescription', { defaultMessage: 'Track how a metric progresses to a goal.', }), + fetchDatatable: true, toExpressionAst, visConfig: { defaults: { diff --git a/src/plugins/vis_types/heatmap/public/to_ast.test.ts b/src/plugins/vis_types/heatmap/public/to_ast.test.ts index 8c7e3372df867..d1e312755cf49 100644 --- a/src/plugins/vis_types/heatmap/public/to_ast.test.ts +++ b/src/plugins/vis_types/heatmap/public/to_ast.test.ts @@ -23,10 +23,6 @@ jest.mock('@kbn/expressions-plugin/public', () => ({ })), })); -jest.mock('./to_ast_esaggs', () => ({ - getEsaggsFn: jest.fn(), -})); - describe('heatmap vis toExpressionAst function', () => { let vis: Vis; @@ -42,7 +38,7 @@ describe('heatmap vis toExpressionAst function', () => { it('should match basic snapshot', () => { toExpressionAst(vis, params); - const [, builtExpression] = (buildExpression as jest.Mock).mock.calls.pop()[0]; + const [builtExpression] = (buildExpression as jest.Mock).mock.calls.pop()[0]; expect(builtExpression).toMatchSnapshot(); }); diff --git a/src/plugins/vis_types/heatmap/public/to_ast.ts b/src/plugins/vis_types/heatmap/public/to_ast.ts index 5b52ab1feeb3a..a5a14f5412dca 100644 --- a/src/plugins/vis_types/heatmap/public/to_ast.ts +++ b/src/plugins/vis_types/heatmap/public/to_ast.ts @@ -10,7 +10,6 @@ import { VisToExpressionAst, getVisSchemas, SchemaConfig } from '@kbn/visualizat import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { getStopsWithColorsFromRanges, getStopsWithColorsFromColorsNumber } from './utils/palette'; import type { HeatmapVisParams } from './types'; -import { getEsaggsFn } from './to_ast_esaggs'; const DEFAULT_PERCENT_DECIMALS = 2; @@ -127,7 +126,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, } visTypeHeatmap.addArgument('palette', buildExpression([palette])); - const ast = buildExpression([getEsaggsFn(vis), visTypeHeatmap]); + const ast = buildExpression([visTypeHeatmap]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts b/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts deleted file mode 100644 index 7a95c59646f45..0000000000000 --- a/src/plugins/vis_types/heatmap/public/to_ast_esaggs.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { Vis } from '@kbn/visualizations-plugin/public'; -import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '@kbn/data-plugin/public'; - -import { HeatmapVisParams } from './types'; - -/** - * Get esaggs expressions function - * @param vis - */ -export function getEsaggsFn(vis: Vis) { - return buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); -} diff --git a/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx b/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx index 6f711eb2667df..ee2893f2cb190 100644 --- a/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx +++ b/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx @@ -28,6 +28,7 @@ export const getHeatmapVisTypeDefinition = ({ description: i18n.translate('visTypeHeatmap.heatmap.heatmapDescription', { defaultMessage: 'Display values as colors in a matrix.', }), + fetchDatatable: true, toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], visConfig: { diff --git a/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap index ef6102571f324..b64b14bd09035 100644 --- a/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap @@ -3,35 +3,6 @@ exports[`metric vis toExpressionAst function with percentage mode should have percentage format 1`] = ` Object { "chain": Array [ - Object { - "arguments": Object { - "aggs": Array [], - "index": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "id": Array [ - "123", - ], - }, - "function": "indexPatternLoad", - "type": "function", - }, - ], - "type": "expression", - }, - ], - "metricsAtAllLevels": Array [ - false, - ], - "partialRows": Array [ - false, - ], - }, - "function": "esaggs", - "type": "function", - }, Object { "arguments": Object { "font": Array [ @@ -96,35 +67,6 @@ Object { exports[`metric vis toExpressionAst function without params 1`] = ` Object { "chain": Array [ - Object { - "arguments": Object { - "aggs": Array [], - "index": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "id": Array [ - "123", - ], - }, - "function": "indexPatternLoad", - "type": "function", - }, - ], - "type": "expression", - }, - ], - "metricsAtAllLevels": Array [ - false, - ], - "partialRows": Array [ - false, - ], - }, - "function": "esaggs", - "type": "function", - }, Object { "arguments": Object { "font": Array [ diff --git a/src/plugins/vis_types/metric/public/metric_vis_type.ts b/src/plugins/vis_types/metric/public/metric_vis_type.ts index 15ec40d3bd612..30e13e8605b6d 100644 --- a/src/plugins/vis_types/metric/public/metric_vis_type.ts +++ b/src/plugins/vis_types/metric/public/metric_vis_type.ts @@ -21,6 +21,7 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition => description: i18n.translate('visTypeMetric.metricDescription', { defaultMessage: 'Show a calculation as a single number.', }), + fetchDatatable: true, toExpressionAst, visConfig: { defaults: { diff --git a/src/plugins/vis_types/metric/public/to_ast.test.ts b/src/plugins/vis_types/metric/public/to_ast.test.ts index bb9a5f0873f11..3c6ba5c532701 100644 --- a/src/plugins/vis_types/metric/public/to_ast.test.ts +++ b/src/plugins/vis_types/metric/public/to_ast.test.ts @@ -22,13 +22,7 @@ describe('metric vis toExpressionAst function', () => { params: { percentageMode: false, }, - data: { - indexPattern: { id: '123' } as any, - aggs: { - getResponseAggs: () => [], - aggs: [], - } as any, - }, + data: {}, } as unknown as Vis; }); diff --git a/src/plugins/vis_types/metric/public/to_ast.ts b/src/plugins/vis_types/metric/public/to_ast.ts index d206d046cde6a..7b771a811ba68 100644 --- a/src/plugins/vis_types/metric/public/to_ast.ts +++ b/src/plugins/vis_types/metric/public/to_ast.ts @@ -11,10 +11,6 @@ import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '@kbn/visualizat import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { inter } from '@kbn/expressions-plugin/common'; -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '@kbn/data-plugin/public'; import { VisParams } from './types'; import { getStopsWithColorsFromRanges } from './utils'; @@ -30,17 +26,6 @@ const prepareDimension = (params: SchemaConfig) => { }; export const toExpressionAst: VisToExpressionAst = (vis, params) => { - const esaggs = buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); - const schemas = getVisSchemas(vis, params); const { @@ -75,7 +60,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, params) => { metricVis.addArgument( 'font', buildExpression( - `font family="${inter.value}" + `font family="${inter.value}" weight="bold" align="center" sizeUnit="pt" @@ -104,7 +89,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, params) => { metricVis.addArgument('metric', prepareDimension(metric)); }); - const ast = buildExpression([esaggs, metricVis]); + const ast = buildExpression([metricVis]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/pie/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/pie/public/__snapshots__/to_ast.test.ts.snap index 5b8bd613609f9..b9dcb3f6bff6a 100644 --- a/src/plugins/vis_types/pie/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/pie/public/__snapshots__/to_ast.test.ts.snap @@ -3,35 +3,6 @@ exports[`vis type pie vis toExpressionAst function should match basic snapshot 1`] = ` Object { "chain": Array [ - Object { - "arguments": Object { - "aggs": Array [], - "index": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "id": Array [ - "123", - ], - }, - "function": "indexPatternLoad", - "type": "function", - }, - ], - "type": "expression", - }, - ], - "metricsAtAllLevels": Array [ - true, - ], - "partialRows": Array [ - false, - ], - }, - "function": "esaggs", - "type": "function", - }, Object { "arguments": Object { "addTooltip": Array [ diff --git a/src/plugins/vis_types/pie/public/to_ast.ts b/src/plugins/vis_types/pie/public/to_ast.ts index 7a131dbb76b9c..91ff6b0b6c17d 100644 --- a/src/plugins/vis_types/pie/public/to_ast.ts +++ b/src/plugins/vis_types/pie/public/to_ast.ts @@ -16,7 +16,6 @@ import { PartitionVisParams, LabelsParams, } from '@kbn/expression-partition-vis-plugin/common'; -import { getEsaggsFn } from './to_ast_esaggs'; const prepareDimension = (params: SchemaConfig) => { const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor }); @@ -83,7 +82,7 @@ export const toExpressionAst: VisToExpressionAst = async (vi args ); - const ast = buildExpression([getEsaggsFn(vis), visTypePie]); + const ast = buildExpression([visTypePie]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/pie/public/to_ast_esaggs.ts b/src/plugins/vis_types/pie/public/to_ast_esaggs.ts deleted file mode 100644 index ed689d065d66c..0000000000000 --- a/src/plugins/vis_types/pie/public/to_ast_esaggs.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { Vis } from '@kbn/visualizations-plugin/public'; -import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '@kbn/data-plugin/public'; -import { PartitionVisParams } from '@kbn/expression-partition-vis-plugin/common'; - -/** - * Get esaggs expressions function - * @param vis - */ -export function getEsaggsFn(vis: Vis) { - return buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); -} diff --git a/src/plugins/vis_types/pie/public/types/types.ts b/src/plugins/vis_types/pie/public/types/types.ts index de8151bffbfcb..968e04fd59075 100644 --- a/src/plugins/vis_types/pie/public/types/types.ts +++ b/src/plugins/vis_types/pie/public/types/types.ts @@ -14,7 +14,7 @@ export interface Dimension { accessor: number; format: { id?: string; - params?: SerializedFieldFormat; + params?: SerializedFieldFormat; }; } diff --git a/src/plugins/vis_types/pie/public/vis_type/pie.ts b/src/plugins/vis_types/pie/public/vis_type/pie.ts index b23f1b3ac4688..113c277d5e210 100644 --- a/src/plugins/vis_types/pie/public/vis_type/pie.ts +++ b/src/plugins/vis_types/pie/public/vis_type/pie.ts @@ -33,6 +33,7 @@ export const getPieVisTypeDefinition = ({ description: i18n.translate('visTypePie.pie.pieDescription', { defaultMessage: 'Compare data in proportion to a whole.', }), + fetchDatatable: true, toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], visConfig: { diff --git a/src/plugins/vis_types/table/public/table_vis_type.ts b/src/plugins/vis_types/table/public/table_vis_type.ts index b4e7a2274852e..8bd20fb6a0c81 100644 --- a/src/plugins/vis_types/table/public/table_vis_type.ts +++ b/src/plugins/vis_types/table/public/table_vis_type.ts @@ -97,7 +97,9 @@ export const tableVisTypeDefinition: VisTypeDefinition = { }, ], }, + fetchDatatable: true, toExpressionAst, + hasPartialRows: (vis) => vis.params.showPartialRows, hierarchicalData: (vis) => vis.params.showPartialRows || vis.params.showMetricsAtAllLevels, requiresSearch: true, }; diff --git a/src/plugins/vis_types/table/public/to_ast.test.ts b/src/plugins/vis_types/table/public/to_ast.test.ts index da112d94c0c18..fc1ebf28c54c5 100644 --- a/src/plugins/vis_types/table/public/to_ast.test.ts +++ b/src/plugins/vis_types/table/public/to_ast.test.ts @@ -58,13 +58,7 @@ describe('table vis toExpressionAst function', () => { showToolbar: false, totalFunc: AggTypes.SUM, }, - data: { - indexPattern: { id: '123' }, - aggs: { - getResponseAggs: () => [], - aggs: [], - }, - }, + data: {}, } as any; }); @@ -75,53 +69,35 @@ describe('table vis toExpressionAst function', () => { it('should create table expression ast', () => { toExpressionAst(vis, {} as any); - expect((buildExpressionFunction as jest.Mock).mock.calls.length).toEqual(5); - expect((buildExpressionFunction as jest.Mock).mock.calls[0]).toEqual([ - 'indexPatternLoad', - { id: '123' }, - ]); - expect((buildExpressionFunction as jest.Mock).mock.calls[1]).toEqual([ - 'esaggs', - { - index: expect.any(Object), - metricsAtAllLevels: false, - partialRows: true, - aggs: [], - }, - ]); + expect(buildExpressionFunction).toHaveBeenCalledTimes(3); // prepare metrics dimensions - expect((buildExpressionFunction as jest.Mock).mock.calls[2]).toEqual([ - 'visdimension', - { accessor: 1 }, - ]); + expect(buildExpressionFunction).nthCalledWith(1, 'visdimension', { accessor: 1 }); // prepare buckets dimensions - expect((buildExpressionFunction as jest.Mock).mock.calls[3]).toEqual([ - 'visdimension', - { accessor: 0 }, - ]); + expect(buildExpressionFunction).nthCalledWith(2, 'visdimension', { accessor: 0 }); // prepare table expression function - expect((buildExpressionFunction as jest.Mock).mock.calls[4]).toEqual([ - 'kibana_table', - { - buckets: [mockTableExpression], - metrics: [mockTableExpression], - perPage: 20, - percentageCol: 'Count', - row: undefined, - showMetricsAtAllLevels: true, - showPartialRows: true, - showToolbar: false, - showTotal: true, - title: undefined, - totalFunc: 'sum', - }, - ]); + expect(buildExpressionFunction).nthCalledWith(3, 'kibana_table', { + buckets: [mockTableExpression], + metrics: [mockTableExpression], + perPage: 20, + percentageCol: 'Count', + row: undefined, + showMetricsAtAllLevels: true, + showPartialRows: true, + showToolbar: false, + showTotal: true, + title: undefined, + totalFunc: 'sum', + }); }); it('should filter out invalid vis params', () => { // @ts-expect-error vis.params.sort = { columnIndex: null }; toExpressionAst(vis, {} as any); - expect((buildExpressionFunction as jest.Mock).mock.calls[4][1].sort).toBeUndefined(); + expect(buildExpressionFunction).nthCalledWith( + 2, + expect.anything(), + expect.not.objectContaining({ sort: expect.anything() }) + ); }); }); diff --git a/src/plugins/vis_types/table/public/to_ast.ts b/src/plugins/vis_types/table/public/to_ast.ts index ac3799fb51cea..a0149b2ba7e6b 100644 --- a/src/plugins/vis_types/table/public/to_ast.ts +++ b/src/plugins/vis_types/table/public/to_ast.ts @@ -6,10 +6,6 @@ * Side Public License, v 1. */ -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '@kbn/data-plugin/public'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '@kbn/visualizations-plugin/public'; import { TableVisParams } from '../common'; @@ -41,17 +37,6 @@ const getMetrics = (schemas: ReturnType, visParams: TableV }; export const toExpressionAst: VisToExpressionAst = (vis, params) => { - const esaggs = buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: vis.params.showPartialRows, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); - const schemas = getVisSchemas(vis, params); const metrics = getMetrics(schemas, vis.params); @@ -81,7 +66,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, params) table.addArgument('splitRow', prepareDimension(schemas.split_row[0])); } - const ast = buildExpression([esaggs, table]); + const ast = buildExpression([table]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap index 3f50cdf559e19..b766feeef5307 100644 --- a/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap @@ -3,35 +3,6 @@ exports[`tagcloud vis toExpressionAst function should match snapshot params fulfilled with DatatableColumn vis_dimension.accessor at metric 1`] = ` Object { "chain": Array [ - Object { - "arguments": Object { - "aggs": Array [], - "index": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "id": Array [ - "123", - ], - }, - "function": "indexPatternLoad", - "type": "function", - }, - ], - "type": "expression", - }, - ], - "metricsAtAllLevels": Array [ - false, - ], - "partialRows": Array [ - false, - ], - }, - "function": "esaggs", - "type": "function", - }, Object { "arguments": Object { "bucket": Array [ @@ -118,35 +89,6 @@ Object { exports[`tagcloud vis toExpressionAst function should match snapshot params fulfilled with number vis_dimension.accessor at metric 1`] = ` Object { "chain": Array [ - Object { - "arguments": Object { - "aggs": Array [], - "index": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "id": Array [ - "123", - ], - }, - "function": "indexPatternLoad", - "type": "function", - }, - ], - "type": "expression", - }, - ], - "metricsAtAllLevels": Array [ - false, - ], - "partialRows": Array [ - false, - ], - }, - "function": "esaggs", - "type": "function", - }, Object { "arguments": Object { "bucket": Array [ @@ -233,35 +175,6 @@ Object { exports[`tagcloud vis toExpressionAst function should match snapshot without params 1`] = ` Object { "chain": Array [ - Object { - "arguments": Object { - "aggs": Array [], - "index": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "id": Array [ - "123", - ], - }, - "function": "indexPatternLoad", - "type": "function", - }, - ], - "type": "expression", - }, - ], - "metricsAtAllLevels": Array [ - false, - ], - "partialRows": Array [ - false, - ], - }, - "function": "esaggs", - "type": "function", - }, Object { "arguments": Object { "bucket": Array [ diff --git a/src/plugins/vis_types/tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_types/tagcloud/public/tag_cloud_type.ts index 35b7845ec515f..417ec9430333d 100644 --- a/src/plugins/vis_types/tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_types/tagcloud/public/tag_cloud_type.ts @@ -38,6 +38,7 @@ export const getTagCloudVisTypeDefinition = ({ palettes }: TagCloudVisDependenci }, }, }, + fetchDatatable: true, toExpressionAst, editorConfig: { enableDataViewChange: true, diff --git a/src/plugins/vis_types/tagcloud/public/to_ast.test.ts b/src/plugins/vis_types/tagcloud/public/to_ast.test.ts index 2c3fcc5799742..e6c70e36c7089 100644 --- a/src/plugins/vis_types/tagcloud/public/to_ast.test.ts +++ b/src/plugins/vis_types/tagcloud/public/to_ast.test.ts @@ -44,13 +44,7 @@ describe('tagcloud vis toExpressionAst function', () => { params: { showLabel: false, }, - data: { - indexPattern: { id: '123' }, - aggs: { - getResponseAggs: () => [], - aggs: [], - }, - }, + data: {}, } as unknown as Vis; }); diff --git a/src/plugins/vis_types/tagcloud/public/to_ast.ts b/src/plugins/vis_types/tagcloud/public/to_ast.ts index 78461475a3d3d..632a4ceb775a0 100644 --- a/src/plugins/vis_types/tagcloud/public/to_ast.ts +++ b/src/plugins/vis_types/tagcloud/public/to_ast.ts @@ -7,10 +7,6 @@ */ import type { PaletteOutput } from '@kbn/coloring'; -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '@kbn/data-plugin/public'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { getVisSchemas, SchemaConfig, VisToExpressionAst } from '@kbn/visualizations-plugin/public'; import { TagCloudVisParams } from './types'; @@ -34,17 +30,6 @@ const preparePalette = (palette?: PaletteOutput) => { }; export const toExpressionAst: VisToExpressionAst = (vis, params) => { - const esaggs = buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); - const schemas = getVisSchemas(vis, params); const { scale, orientation, minFontSize, maxFontSize, showLabel, palette } = vis.params; @@ -62,7 +47,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, para tagcloud.addArgument('bucket', prepareDimension(schemas.segment[0])); } - const ast = buildExpression([esaggs, tagcloud]); + const ast = buildExpression([tagcloud]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts index 1bf4fb4f24a60..ad1be0fd1c50e 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/check_if_series_have_same_formatters.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { FieldFormatMap } from '@kbn/data-plugin/common'; +import type { FieldFormatMap } from '@kbn/data-views-plugin/common'; import { DATA_FORMATTERS } from '../../../../common/enums'; import { aggs } from '../../../../common/agg_utils'; import type { Series } from '../../../../common/types'; diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts b/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts index 0d605618cf864..2836437972401 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/create_field_formatter.ts @@ -8,7 +8,7 @@ import { isNumber } from 'lodash'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; -import type { FieldFormatMap } from '@kbn/data-plugin/common'; +import type { FieldFormatMap } from '@kbn/data-views-plugin/common'; import type { FieldFormatsContentType } from '@kbn/field-formats-plugin/common'; import { isEmptyValue, DISPLAY_EMPTY_VALUE } from '../../../../common/last_value_utils'; import { getFieldFormats } from '../../../services'; diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts index 68be45ff6eec0..f309e3041424e 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/index.ts @@ -11,7 +11,7 @@ import { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; import { IUiSettingsClient } from '@kbn/core/public'; import { PersistedState } from '@kbn/visualizations-plugin/public'; import type { PaletteRegistry } from '@kbn/coloring'; -import type { FieldFormatMap } from '@kbn/data-plugin/common'; +import type { FieldFormatMap } from '@kbn/data-views-plugin/common'; import { TimeseriesVisParams } from '../../../types'; import type { TimeseriesVisData, PanelData } from '../../../../common/types'; import { FetchedIndexPattern } from '../../../../common/types'; diff --git a/src/plugins/vis_types/timeseries/public/trigger_action/get_series.test.ts b/src/plugins/vis_types/timeseries/public/trigger_action/get_series.test.ts index cfd858a345669..40fa06ba427a1 100644 --- a/src/plugins/vis_types/timeseries/public/trigger_action/get_series.test.ts +++ b/src/plugins/vis_types/timeseries/public/trigger_action/get_series.test.ts @@ -104,7 +104,7 @@ describe('getSeries', () => { fieldName: 'document', isFullReference: true, params: { - formula: 'clamp(max(day_of_week_i), 0, max(day_of_week_i))', + formula: 'pick_max(max(day_of_week_i), 0)', }, }, ]); diff --git a/src/plugins/vis_types/timeseries/public/trigger_action/metrics_helpers.ts b/src/plugins/vis_types/timeseries/public/trigger_action/metrics_helpers.ts index e3d8fa0434cbd..f6a368382b5b4 100644 --- a/src/plugins/vis_types/timeseries/public/trigger_action/metrics_helpers.ts +++ b/src/plugins/vis_types/timeseries/public/trigger_action/metrics_helpers.ts @@ -220,7 +220,10 @@ export const getSiblingPipelineSeriesFormula = ( // support nested aggs with formula const additionalSubFunction = metrics.find((metric) => metric.id === subMetricField); let formula = `${aggregationMap.name}(`; - let minMax = ''; + let minimumValue = ''; + if (currentMetric.type === 'positive_only') { + minimumValue = `, 0`; + } if (additionalSubFunction) { const additionalPipelineAggMap = SUPPORTED_METRICS[additionalSubFunction.type]; if (!additionalPipelineAggMap) { @@ -228,14 +231,9 @@ export const getSiblingPipelineSeriesFormula = ( } const additionalSubFunctionField = additionalSubFunction.type !== 'count' ? additionalSubFunction.field : ''; - if (currentMetric.type === 'positive_only') { - minMax = `, 0, ${pipelineAggMap.name}(${additionalPipelineAggMap.name}(${ - additionalSubFunctionField ?? '' - }))`; - } formula += `${pipelineAggMap.name}(${additionalPipelineAggMap.name}(${ additionalSubFunctionField ?? '' - }))${minMax})`; + }))${minimumValue})`; } else { let additionalFunctionArgs; // handle percentile and percentile_rank @@ -246,14 +244,9 @@ export const getSiblingPipelineSeriesFormula = ( if (pipelineAggMap.name === 'percentile_rank' && nestedMetaValue) { additionalFunctionArgs = `, value=${nestedMetaValue}`; } - if (currentMetric.type === 'positive_only') { - minMax = `, 0, ${pipelineAggMap.name}(${subMetricField ?? ''}${ - additionalFunctionArgs ? `${additionalFunctionArgs}` : '' - })`; - } formula += `${pipelineAggMap.name}(${subMetricField ?? ''}${ additionalFunctionArgs ? `${additionalFunctionArgs}` : '' - })${minMax})`; + })${minimumValue})`; } return formula; }; diff --git a/src/plugins/vis_types/timeseries/public/trigger_action/supported_metrics.ts b/src/plugins/vis_types/timeseries/public/trigger_action/supported_metrics.ts index 30b6f47da5f7e..29bf2008e208d 100644 --- a/src/plugins/vis_types/timeseries/public/trigger_action/supported_metrics.ts +++ b/src/plugins/vis_types/timeseries/public/trigger_action/supported_metrics.ts @@ -93,7 +93,7 @@ export const SUPPORTED_METRICS: { [key: string]: AggOptions } = { isFullReference: true, }, positive_only: { - name: 'clamp', + name: 'pick_max', isFullReference: true, }, static: { diff --git a/src/plugins/vis_types/vislib/public/to_ast.test.ts b/src/plugins/vis_types/vislib/public/to_ast.test.ts index 1628fb1812d34..a7bbf146005b4 100644 --- a/src/plugins/vis_types/vislib/public/to_ast.test.ts +++ b/src/plugins/vis_types/vislib/public/to_ast.test.ts @@ -23,10 +23,6 @@ jest.mock('@kbn/expressions-plugin/public', () => ({ })), })); -jest.mock('./to_ast_esaggs', () => ({ - getEsaggsFn: jest.fn(), -})); - describe('vislib vis toExpressionAst function', () => { let vis: Vis; @@ -42,7 +38,7 @@ describe('vislib vis toExpressionAst function', () => { it('should match basic snapshot', () => { toExpressionAst(vis, params); - const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + const [builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; expect(builtExpression).toMatchSnapshot(); }); diff --git a/src/plugins/vis_types/vislib/public/to_ast.ts b/src/plugins/vis_types/vislib/public/to_ast.ts index 6ef3ec72f4ab0..ceb938d5d72e1 100644 --- a/src/plugins/vis_types/vislib/public/to_ast.ts +++ b/src/plugins/vis_types/vislib/public/to_ast.ts @@ -22,7 +22,6 @@ import { BUCKET_TYPES } from '@kbn/data-plugin/public'; import { vislibVisName, VisTypeVislibExpressionFunctionDefinition } from './vis_type_vislib_vis_fn'; import { BasicVislibParams, VislibChartType } from './types'; -import { getEsaggsFn } from './to_ast_esaggs'; export const toExpressionAst = async ( vis: Vis, @@ -95,7 +94,7 @@ export const toExpressionAst = async ( } ); - const ast = buildExpression([getEsaggsFn(vis), visTypeVislib]); + const ast = buildExpression([visTypeVislib]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/vislib/public/to_ast_esaggs.ts b/src/plugins/vis_types/vislib/public/to_ast_esaggs.ts deleted file mode 100644 index 6874f812c41ff..0000000000000 --- a/src/plugins/vis_types/vislib/public/to_ast_esaggs.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { Vis } from '@kbn/visualizations-plugin/public'; -import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '@kbn/data-plugin/public'; - -/** - * Get esaggs expressions function - * TODO: replace this with vis.data.aggs!.toExpressionAst(); - * https://github.com/elastic/kibana/issues/61768 - * @param vis - */ -export function getEsaggsFn(vis: Vis) { - return buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); -} diff --git a/src/plugins/vis_types/vislib/public/to_ast_pie.test.ts b/src/plugins/vis_types/vislib/public/to_ast_pie.test.ts index 49a44c3de4a89..9cb57c8086db1 100644 --- a/src/plugins/vis_types/vislib/public/to_ast_pie.test.ts +++ b/src/plugins/vis_types/vislib/public/to_ast_pie.test.ts @@ -23,10 +23,6 @@ jest.mock('@kbn/expressions-plugin/public', () => ({ })), })); -jest.mock('./to_ast_esaggs', () => ({ - getEsaggsFn: jest.fn(), -})); - describe('vislib pie vis toExpressionAst function', () => { let vis: Vis; @@ -42,7 +38,7 @@ describe('vislib pie vis toExpressionAst function', () => { it('should match basic snapshot', () => { toExpressionAst(vis, params); - const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + const [builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; expect(builtExpression).toMatchSnapshot(); }); diff --git a/src/plugins/vis_types/vislib/public/to_ast_pie.ts b/src/plugins/vis_types/vislib/public/to_ast_pie.ts index 9f7bda7740a44..3302130df0134 100644 --- a/src/plugins/vis_types/vislib/public/to_ast_pie.ts +++ b/src/plugins/vis_types/vislib/public/to_ast_pie.ts @@ -11,7 +11,6 @@ import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugi import { PieVisParams } from './pie'; import { vislibPieName, VisTypeVislibPieExpressionFunctionDefinition } from './pie_fn'; -import { getEsaggsFn } from './to_ast_esaggs'; export const toExpressionAst: VisToExpressionAst = async (vis, params) => { const schemas = getVisSchemas(vis, params); @@ -32,7 +31,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, par } ); - const ast = buildExpression([getEsaggsFn(vis), visTypePie]); + const ast = buildExpression([visTypePie]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/xy/public/to_ast.test.ts b/src/plugins/vis_types/xy/public/to_ast.test.ts index ed35017d53edf..e9b597786c33e 100644 --- a/src/plugins/vis_types/xy/public/to_ast.test.ts +++ b/src/plugins/vis_types/xy/public/to_ast.test.ts @@ -23,10 +23,6 @@ jest.mock('@kbn/expressions-plugin/public', () => ({ })), })); -jest.mock('./to_ast_esaggs', () => ({ - getEsaggsFn: jest.fn(), -})); - describe('xy vis toExpressionAst function', () => { let vis: Vis; @@ -42,7 +38,7 @@ describe('xy vis toExpressionAst function', () => { it('should match basic snapshot', () => { toExpressionAst(vis, params); - const [, builtExpression] = (buildExpression as jest.Mock).mock.calls.pop()[0]; + const [builtExpression] = (buildExpression as jest.Mock).mock.calls.pop()[0]; expect(builtExpression).toMatchSnapshot(); }); diff --git a/src/plugins/vis_types/xy/public/to_ast.ts b/src/plugins/vis_types/xy/public/to_ast.ts index bf2ca297f9f38..46ff05f4426a6 100644 --- a/src/plugins/vis_types/xy/public/to_ast.ts +++ b/src/plugins/vis_types/xy/public/to_ast.ts @@ -32,7 +32,6 @@ import { } from './types'; import { visName, VisTypeXyExpressionFunctionDefinition } from './expression_functions/xy_vis_fn'; import { XyVisType } from '../common'; -import { getEsaggsFn } from './to_ast_esaggs'; import { getSeriesParams } from './utils/get_series_params'; import { getSafeId } from './utils/accessors'; @@ -238,7 +237,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params splitColumnDimension: dimensions.splitColumn?.map(prepareXYDimension), }); - const ast = buildExpression([getEsaggsFn(vis), visTypeXy]); + const ast = buildExpression([visTypeXy]); return ast.toAst(); }; diff --git a/src/plugins/vis_types/xy/public/to_ast_esaggs.ts b/src/plugins/vis_types/xy/public/to_ast_esaggs.ts deleted file mode 100644 index 1a9079079ebda..0000000000000 --- a/src/plugins/vis_types/xy/public/to_ast_esaggs.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { Vis } from '@kbn/visualizations-plugin/public'; -import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '@kbn/data-plugin/public'; - -import { VisParams } from './types'; - -/** - * Get esaggs expressions function - * TODO: replace this with vis.data.aggs!.toExpressionAst(); - * https://github.com/elastic/kibana/issues/61768 - * @param vis - */ -export function getEsaggsFn(vis: Vis) { - return buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: vis.isHierarchical(), - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); -} diff --git a/src/plugins/vis_types/xy/public/vis_types/area.ts b/src/plugins/vis_types/xy/public/vis_types/area.ts index 84a6a65d2753a..0ca07c8067457 100644 --- a/src/plugins/vis_types/xy/public/vis_types/area.ts +++ b/src/plugins/vis_types/xy/public/vis_types/area.ts @@ -35,6 +35,7 @@ export const areaVisTypeDefinition = { description: i18n.translate('visTypeXy.area.areaDescription', { defaultMessage: 'Emphasize the data between an axis and a line.', }), + fetchDatatable: true, toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], updateVisTypeOnParamsChange: getVisTypeFromParams, diff --git a/src/plugins/vis_types/xy/public/vis_types/histogram.ts b/src/plugins/vis_types/xy/public/vis_types/histogram.ts index dd1ee2836b10f..680186eb330f9 100644 --- a/src/plugins/vis_types/xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_types/xy/public/vis_types/histogram.ts @@ -37,6 +37,7 @@ export const histogramVisTypeDefinition = { description: i18n.translate('visTypeXy.histogram.histogramDescription', { defaultMessage: 'Present data in vertical bars on an axis.', }), + fetchDatatable: true, toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], updateVisTypeOnParamsChange: getVisTypeFromParams, diff --git a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts index dda1ead899faf..25fc3142e0e98 100644 --- a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts @@ -37,6 +37,7 @@ export const horizontalBarVisTypeDefinition = { description: i18n.translate('visTypeXy.horizontalBar.horizontalBarDescription', { defaultMessage: 'Present data in horizontal bars on an axis.', }), + fetchDatatable: true, toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], updateVisTypeOnParamsChange: getVisTypeFromParams, diff --git a/src/plugins/vis_types/xy/public/vis_types/line.ts b/src/plugins/vis_types/xy/public/vis_types/line.ts index a4ad14d7f5442..e0c7e081573f3 100644 --- a/src/plugins/vis_types/xy/public/vis_types/line.ts +++ b/src/plugins/vis_types/xy/public/vis_types/line.ts @@ -35,6 +35,7 @@ export const lineVisTypeDefinition = { description: i18n.translate('visTypeXy.line.lineDescription', { defaultMessage: 'Display data as a series of points.', }), + fetchDatatable: true, toExpressionAst, getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], updateVisTypeOnParamsChange: getVisTypeFromParams, diff --git a/src/plugins/visualizations/common/utils/accessors.ts b/src/plugins/visualizations/common/utils/accessors.ts index e8146906b036a..97b47ef7a3267 100644 --- a/src/plugins/visualizations/common/utils/accessors.ts +++ b/src/plugins/visualizations/common/utils/accessors.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common'; -import { FieldFormatParams, SerializedFieldFormat } from '@kbn/field-formats-plugin/common/types'; +import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common/types'; import { ExpressionValueVisDimension } from '../expression_functions'; const getAccessorByIndex = (accessor: number, columns: Datatable['columns']) => @@ -68,7 +68,7 @@ export function getAccessor(dimension: string | ExpressionValueVisDimension) { export function getFormatByAccessor( dimension: string | ExpressionValueVisDimension, columns: DatatableColumn[], - defaultColumnFormat?: SerializedFieldFormat + defaultColumnFormat?: SerializedFieldFormat ) { return typeof dimension === 'string' ? getColumnByAccessor(dimension, columns)?.meta.params || defaultColumnFormat diff --git a/src/plugins/visualizations/public/embeddable/to_ast.ts b/src/plugins/visualizations/public/embeddable/to_ast.ts index dc4e931781f7b..80e7217f8d1c1 100644 --- a/src/plugins/visualizations/public/embeddable/to_ast.ts +++ b/src/plugins/visualizations/public/embeddable/to_ast.ts @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import { ExpressionFunctionKibana, ExpressionFunctionKibanaContext } from '@kbn/data-plugin/public'; -import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; +import { ExpressionFunctionKibana } from '@kbn/data-plugin/public'; +import { ExpressionAstExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; -import { queryToAst, filtersToAst } from '@kbn/data-plugin/common'; import { VisToExpressionAst } from '../types'; /** @@ -19,31 +18,38 @@ import { VisToExpressionAst } from '../types'; * * @internal */ -export const toExpressionAst: VisToExpressionAst = async (vis, params) => { - const { savedSearchId, searchSource } = vis.data; - const query = searchSource?.getField('query'); - let filters = searchSource?.getField('filter'); - if (typeof filters === 'function') { - filters = filters(); +export const toExpressionAst: VisToExpressionAst = async ( + vis, + params +): Promise => { + if (!vis.type.toExpressionAst) { + throw new Error('Visualization type definition should have toExpressionAst function defined'); } - const kibana = buildExpressionFunction('kibana', {}); - const kibanaContext = buildExpressionFunction('kibana_context', { - q: query && queryToAst(query), - filters: filters && filtersToAst(filters), - savedSearchId, - }); - - const ast = buildExpression([kibana, kibanaContext]); - const expression = ast.toAst(); + const searchSource = vis.data.searchSource?.createCopy(); - if (!vis.type.toExpressionAst) { - throw new Error('Visualization type definition should have toExpressionAst function defined'); + if (vis.data.aggs) { + vis.data.aggs.hierarchical = vis.isHierarchical(); + vis.data.aggs.partialRows = + typeof vis.type.hasPartialRows === 'function' + ? vis.type.hasPartialRows(vis) + : vis.type.hasPartialRows; + searchSource?.setField('aggs', vis.data.aggs); } const visExpressionAst = await vis.type.toExpressionAst(vis, params); - // expand the expression chain with a particular visualization expression chain, if it exists - expression.chain.push(...visExpressionAst.chain); + const searchSourceExpressionAst = searchSource?.toExpressionAst({ + asDatatable: vis.type.fetchDatatable, + }); + + const expression = { + ...visExpressionAst, + chain: [ + buildExpressionFunction('kibana', {}).toAst(), + ...(searchSourceExpressionAst?.chain ?? []), + ...visExpressionAst.chain, + ], + }; return expression; }; diff --git a/src/plugins/visualizations/public/vis.scss b/src/plugins/visualizations/public/vis.scss index 42b59b8de93cd..42ffab11a1eda 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/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 5b1e2afc91dfd..18dcacadcaeab 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -40,10 +40,12 @@ export class BaseVisType { public readonly editorConfig; public hidden; public readonly requiresSearch; + public readonly hasPartialRows; public readonly hierarchicalData; public readonly setup; public readonly getUsedIndexPattern; public readonly inspectorAdapters; + public readonly fetchDatatable: boolean; public readonly toExpressionAst; public readonly getInfoMessage; public readonly updateVisTypeOnParamsChange; @@ -72,9 +74,11 @@ export class BaseVisType { this.hidden = opts.hidden ?? false; this.requiresSearch = opts.requiresSearch ?? false; this.setup = opts.setup; + this.hasPartialRows = opts.hasPartialRows ?? false; this.hierarchicalData = opts.hierarchicalData ?? false; this.getUsedIndexPattern = opts.getUsedIndexPattern; this.inspectorAdapters = opts.inspectorAdapters; + this.fetchDatatable = opts.fetchDatatable ?? false; this.toExpressionAst = opts.toExpressionAst; this.getInfoMessage = opts.getInfoMessage; this.updateVisTypeOnParamsChange = opts.updateVisTypeOnParamsChange; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 73b3f96ab2ea7..8f6dc309a7145 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -216,6 +216,10 @@ export interface VisTypeDefinition { * with the selection of a search source - an index pattern or a saved search. */ readonly requiresSearch?: boolean; + /** + * In case when the visualization performs an aggregation, this option will be used to display or hide the rows with partial data. + */ + readonly hasPartialRows?: boolean | ((vis: { params: TVisParams }) => boolean); readonly hierarchicalData?: boolean | ((vis: { params: TVisParams }) => boolean); readonly inspectorAdapters?: Adapters | (() => Adapters); /** @@ -225,6 +229,12 @@ export interface VisTypeDefinition { * of this type. */ readonly getInfoMessage?: (vis: Vis) => React.ReactNode; + + /** + * When truthy, it will perform a search and pass the results to the visualization as a `datatable`. + * @default false + */ + readonly fetchDatatable?: boolean; /** * Should be provided to expand base visualization expression with * custom exprsesion chain, including render expression. diff --git a/test/api_integration/apis/index_patterns/constants.ts b/test/api_integration/apis/index_patterns/constants.ts index 8194966bcdcb8..eaec583f72d59 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 0000000000000..60c5ae1dc0935 --- /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 0000000000000..cce2da6cd89f9 --- /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 81d605d217b54..158fe3087bcbe 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/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 52218b88be60d..126788c3312d2 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -7,6 +7,7 @@ */ import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../ftr_provider_context'; const DEFAULT_REQUEST = ` @@ -24,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'console']); + const PageObjects = getPageObjects(['common', 'console', 'header']); const toasts = getService('toasts'); describe('console app', function describeIndexTests() { @@ -122,5 +123,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); + + describe('multiple requests output', () => { + const sendMultipleRequests = async (requests: string[]) => { + await asyncForEach(requests, async (request) => { + await PageObjects.console.enterRequest(request); + }); + await PageObjects.console.selectAllRequests(); + await PageObjects.console.clickPlay(); + }; + + beforeEach(async () => { + await PageObjects.console.clearTextArea(); + }); + + it('should contain comments starting with # symbol', async () => { + await sendMultipleRequests(['\n PUT test-index', '\n DELETE test-index']); + await retry.try(async () => { + const response = await PageObjects.console.getResponse(); + log.debug(response); + expect(response).to.contain('# PUT test-index 200 OK'); + expect(response).to.contain('# DELETE test-index 200 OK'); + }); + }); + + it('should display status badges', async () => { + await sendMultipleRequests(['\n GET _search/test', '\n GET _search']); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.console.hasWarningBadge()).to.be(true); + expect(await PageObjects.console.hasSuccessBadge()).to.be(true); + }); + }); }); } diff --git a/test/functional/apps/console/_console_ccs.ts b/test/functional/apps/console/_console_ccs.ts new file mode 100644 index 0000000000000..3ab81e076158b --- /dev/null +++ b/test/functional/apps/console/_console_ccs.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const log = getService('log'); + const PageObjects = getPageObjects(['common', 'console']); + const remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); + + describe('Console App CCS', function describeIndexTests() { + this.tags('includeFirefox'); + before(async () => { + await remoteEsArchiver.loadIfNeeded( + 'test/functional/fixtures/es_archiver/logstash_functional' + ); + log.debug('navigateTo console'); + await PageObjects.common.navigateToApp('console'); + await retry.try(async () => { + await PageObjects.console.collapseHelp(); + }); + }); + + after(async () => { + await remoteEsArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + }); + + describe('Perform CCS Search in Console', () => { + before(async () => { + await PageObjects.console.clearTextArea(); + }); + it('it should be able to access remote data', async () => { + await PageObjects.console.enterRequest('\nGET ftr-remote:logstash-*/_search'); + await PageObjects.console.clickPlay(); + await retry.try(async () => { + const actualResponse = await PageObjects.console.getResponse(); + expect(actualResponse).to.contain('"extension": "jpg",'); + }); + }); + }); + }); +} diff --git a/test/functional/apps/console/index.js b/test/functional/apps/console/index.js index 1944e10b5239f..4f0c362268b6f 100644 --- a/test/functional/apps/console/index.js +++ b/test/functional/apps/console/index.js @@ -8,14 +8,18 @@ export default function ({ getService, loadTestFile }) { const browser = getService('browser'); + const config = getService('config'); describe('console app', function () { before(async function () { await browser.setWindowSize(1300, 1100); }); - - loadTestFile(require.resolve('./_console')); - loadTestFile(require.resolve('./_autocomplete')); - loadTestFile(require.resolve('./_vector_tile')); + if (config.get('esTestCluster.ccs')) { + loadTestFile(require.resolve('./_console_ccs')); + } else { + loadTestFile(require.resolve('./_console')); + loadTestFile(require.resolve('./_autocomplete')); + loadTestFile(require.resolve('./_vector_tile')); + } }); } diff --git a/test/functional/apps/discover/_data_grid_copy_to_clipboard.ts b/test/functional/apps/discover/_data_grid_copy_to_clipboard.ts new file mode 100644 index 0000000000000..f202595729cfb --- /dev/null +++ b/test/functional/apps/discover/_data_grid_copy_to_clipboard.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dataGrid = getService('dataGrid'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); + const toasts = getService('toasts'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'dashboard']); + const defaultSettings = { + defaultIndex: 'logstash-*', + 'doc_table:legacy': false, + }; + + describe('discover data grid supports copy to clipboard', function describeIndexTests() { + before(async function () { + log.debug('load kibana index with default index pattern'); + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + }); + + beforeEach(async () => { + await PageObjects.common.navigateToApp('discover'); + }); + + after(async function () { + log.debug('reset uiSettings'); + await kibanaServer.uiSettings.replace({}); + }); + + it('should be able to copy a column values to clipboard', async () => { + const canReadClipboard = await browser.checkBrowserPermission('clipboard-read'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await dataGrid.clickCopyColumnValues('@timestamp'); + + if (canReadClipboard) { + const copiedTimestampData = await browser.getClipboardValue(); + expect( + copiedTimestampData.startsWith( + '"\'@timestamp"\n"Sep 22, 2015 @ 23:50:13.253"\n"Sep 22, 2015 @ 23:43:58.175"' + ) + ).to.be(true); + } + + expect(await toasts.getToastCount()).to.be(1); + await toasts.dismissAllToasts(); + + await dataGrid.clickCopyColumnValues('_source'); + + if (canReadClipboard) { + const copiedSourceData = await browser.getClipboardValue(); + expect(copiedSourceData.startsWith('"_source"\n{"@message":["238.171.34.42')).to.be(true); + expect(copiedSourceData.endsWith('}')).to.be(true); + } + + expect(await toasts.getToastCount()).to.be(1); + await toasts.dismissAllToasts(); + }); + + it('should be able to copy a column name to clipboard', async () => { + const canReadClipboard = await browser.checkBrowserPermission('clipboard-read'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await dataGrid.clickCopyColumnName('@timestamp'); + if (canReadClipboard) { + const copiedTimestampName = await browser.getClipboardValue(); + expect(copiedTimestampName).to.be('"\'@timestamp"'); + } + + expect(await toasts.getToastCount()).to.be(1); + await toasts.dismissAllToasts(); + + await dataGrid.clickCopyColumnName('_source'); + + if (canReadClipboard) { + const copiedSourceName = await browser.getClipboardValue(); + expect(copiedSourceName).to.be('"_source"'); + } + + expect(await toasts.getToastCount()).to.be(1); + await toasts.dismissAllToasts(); + }); + }); +} diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 5932e995421af..d3d5ea346ddea 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -28,8 +28,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.load('test/functional/fixtures/es_archiver/long_window_logstash'); - await esArchiver.load( - 'test/functional/fixtures/es_archiver/long_window_logstash_index_pattern' + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/long_window_logstash_index_pattern' ); await security.testUser.setRoles(['kibana_admin', 'long_window_logstash']); await kibanaServer.uiSettings.replace(defaultSettings); @@ -37,9 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { await esArchiver.unload('test/functional/fixtures/es_archiver/long_window_logstash'); - await esArchiver.unload( - 'test/functional/fixtures/es_archiver/long_window_logstash_index_pattern' - ); + await kibanaServer.savedObjects.cleanStandardList(); await security.testUser.restoreDefaults(); await PageObjects.common.unsetTime(); }); diff --git a/test/functional/apps/discover/_doc_table_newline.ts b/test/functional/apps/discover/_doc_table_newline.ts index 94bf23a70bc60..a02d31d5c76f4 100644 --- a/test/functional/apps/discover/_doc_table_newline.ts +++ b/test/functional/apps/discover/_doc_table_newline.ts @@ -21,15 +21,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_message_with_newline']); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/message_with_newline'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/message_with_newline.json' + ); await kibanaServer.uiSettings.replace({ defaultIndex: 'newline-test', 'doc_table:legacy': true, }); await PageObjects.common.navigateToApp('discover'); }); + after(async () => { await security.testUser.restoreDefaults(); await esArchiver.unload('test/functional/fixtures/es_archiver/message_with_newline'); + await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.uiSettings.unset('defaultIndex'); await kibanaServer.uiSettings.unset('doc_table:legacy'); }); diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index fa3c37fc1a694..431d77f1b4b5e 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -50,6 +50,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_data_grid_field_data')); loadTestFile(require.resolve('./_data_grid_doc_navigation')); loadTestFile(require.resolve('./_data_grid_doc_table')); + loadTestFile(require.resolve('./_data_grid_copy_to_clipboard')); loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields')); loadTestFile(require.resolve('./_runtime_fields_editor')); loadTestFile(require.resolve('./_huge_fields')); diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 23122312059c7..1801175247474 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -17,7 +17,6 @@ function uniq(input: T[]): T[] { export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); - const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); const testSubjects = getService('testSubjects'); const log = getService('log'); @@ -25,14 +24,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { - await esArchiver.load('test/functional/fixtures/es_archiver/management'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/management'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); }); afterEach(async function () { - await esArchiver.unload('test/functional/fixtures/es_archiver/management'); + await kibanaServer.savedObjects.cleanStandardList(); }); it('should import saved objects', async function () { diff --git a/test/functional/apps/management/_scripted_fields_filter.ts b/test/functional/apps/management/_scripted_fields_filter.ts index 1093c7f82fd4a..bfd881d1e1149 100644 --- a/test/functional/apps/management/_scripted_fields_filter.ts +++ b/test/functional/apps/management/_scripted_fields_filter.ts @@ -14,21 +14,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const browser = getService('browser'); - const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['settings']); describe('filter scripted fields', function describeIndexTests() { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await browser.setWindowSize(1200, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/management'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/management'); await kibanaServer.uiSettings.replace({ defaultIndex: 'f1e4c910-a2e6-11e7-bb30-233be9be6a15', }); }); after(async function () { - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + await kibanaServer.savedObjects.cleanStandardList(); }); const scriptedPainlessFieldName = 'ram_pain1'; diff --git a/test/functional/apps/saved_objects_management/inspect_saved_objects.ts b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts index 059f172e51b46..7dc609bb28840 100644 --- a/test/functional/apps/saved_objects_management/inspect_saved_objects.ts +++ b/test/functional/apps/saved_objects_management/inspect_saved_objects.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export default function ({ getPageObjects, getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']); const find = getService('find'); @@ -33,18 +33,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { return bools.every((currBool) => currBool === true); }; - // FLAKY: https://github.com/elastic/kibana/issues/118288 - describe.skip('saved objects inspect page', () => { + describe('saved objects inspect page', () => { beforeEach(async () => { - await esArchiver.load( - 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/saved_objects_management/edit_saved_object' ); }); afterEach(async () => { - await esArchiver.unload( - 'test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object' - ); + await kibanaServer.savedObjects.cleanStandardList(); }); it('allows to view the saved object', async () => { diff --git a/test/functional/apps/saved_objects_management/show_relationships.ts b/test/functional/apps/saved_objects_management/show_relationships.ts index 26fd2a00430f1..a48e8069fe436 100644 --- a/test/functional/apps/saved_objects_management/show_relationships.ts +++ b/test/functional/apps/saved_objects_management/show_relationships.ts @@ -10,20 +10,18 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']); describe('saved objects relationships flyout', () => { beforeEach(async () => { - await esArchiver.load( - 'test/functional/fixtures/es_archiver/saved_objects_management/show_relationships' + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/saved_objects_management/show_relationships' ); }); afterEach(async () => { - await esArchiver.unload( - 'test/functional/fixtures/es_archiver/saved_objects_management/show_relationships' - ); + await kibanaServer.savedObjects.cleanStandardList(); }); it('displays the invalid references', async () => { diff --git a/test/functional/config.ccs.ts b/test/functional/config.ccs.ts index 81a1438013c55..dfa61f654e092 100644 --- a/test/functional/config.ccs.ts +++ b/test/functional/config.ccs.ts @@ -17,7 +17,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...baseConfig.getAll(), - testFiles: [require.resolve('./apps/dashboard/group3'), require.resolve('./apps/discover')], + testFiles: [ + require.resolve('./apps/dashboard/group3'), + require.resolve('./apps/discover'), + require.resolve('./apps/console/_console_ccs'), + ], services: { ...baseConfig.get('services'), diff --git a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json index 9998cb3a71732..aea1bf770c18f 100644 --- a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json +++ b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json @@ -14,22 +14,6 @@ } } -{ - "type": "doc", - "value": { - "id": "index-pattern:with-timefield", - "index": ".kibana", - "source": { - "index-pattern": { - "fields": "[]", - "title": "with-timefield", - "timeFieldName": "@timestamp" - }, - "type": "index-pattern" - } - } -} - { "type": "doc", "value": { diff --git a/test/functional/fixtures/es_archiver/long_window_logstash_index_pattern/data.json b/test/functional/fixtures/es_archiver/long_window_logstash_index_pattern/data.json deleted file mode 100644 index 75aa6f06bb11a..0000000000000 --- a/test/functional/fixtures/es_archiver/long_window_logstash_index_pattern/data.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "doc", - "value": { - "id": "index-pattern:long-window-logstash-*", - "index": ".kibana", - "source": { - "index-pattern": { - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", - "timeFieldName": "@timestamp", - "title": "long-window-logstash-*" - }, - "type": "index-pattern" - } - } -} - - diff --git a/test/functional/fixtures/es_archiver/management/data.json.gz b/test/functional/fixtures/es_archiver/management/data.json.gz deleted file mode 100644 index cfb08a5ee3a60..0000000000000 Binary files a/test/functional/fixtures/es_archiver/management/data.json.gz and /dev/null differ diff --git a/test/functional/fixtures/es_archiver/management/mappings.json b/test/functional/fixtures/es_archiver/management/mappings.json deleted file mode 100644 index 451369d85acd8..0000000000000 --- a/test/functional/fixtures/es_archiver/management/mappings.json +++ /dev/null @@ -1,205 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "dashboard": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "index-pattern": { - "dynamic": "strict", - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "dynamic": "strict", - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "dynamic": "strict", - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "url": { - "dynamic": "strict", - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/message_with_newline/data.json b/test/functional/fixtures/es_archiver/message_with_newline/data.json index 3611f2d3878a4..62a2b5bf66fa8 100644 --- a/test/functional/fixtures/es_archiver/message_with_newline/data.json +++ b/test/functional/fixtures/es_archiver/message_with_newline/data.json @@ -1,18 +1,3 @@ -{ - "type": "doc", - "value": { - "id": "index-pattern:newline-test", - "index": ".kibana", - "source": { - "index-pattern": { - "fields": "[]", - "title": "newline-test" - }, - "type": "index-pattern" - } - } -} - { "type": "doc", "value": { diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/data.json deleted file mode 100644 index cbaa0306f9a42..0000000000000 --- a/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/data.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "index-pattern:logstash-*", - "source": { - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", - "source": { - "visualization": { - "title": "A Pie", - "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - }, - "type": "visualization", - "updated_at": "2019-01-22T19:32:31.206Z" - }, - "references" : [ - { - "name" : "kibanaSavedObjectMeta.searchSourceJSON.index", - "type" : "index-pattern", - "id" : "logstash-*" - } - ] - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "dashboard:i-exist", - "source": { - "dashboard": { - "title": "A Dashboard", - "hits": 0, - "description": "", - "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", - "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", - "version": 1, - "timeRestore": false, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - }, - "type": "dashboard", - "updated_at": "2019-01-22T19:32:47.232Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "config:6.0.0", - "source": { - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json deleted file mode 100644 index bb863dc24c585..0000000000000 --- a/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json +++ /dev/null @@ -1,421 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, - "mappings": { - "dynamic": "strict", - "properties": { - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "map": { - "properties": { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaceId": { - "type": "keyword" - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - } - } -} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json deleted file mode 100644 index 4d5b969a3c931..0000000000000 --- a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "dashboard:dash-with-missing-refs", - "source": { - "dashboard": { - "title": "Dashboard with missing refs", - "hits": 0, - "description": "", - "panelsJSON": "[]", - "optionsJSON": "{}", - "version": 1, - "timeRestore": false, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{}" - } - }, - "type": "dashboard", - "references": [ - { - "type": "visualization", - "id": "missing-vis-ref", - "name": "some missing ref" - }, - { - "type": "dashboard", - "id": "missing-dashboard-ref", - "name": "some other missing ref" - } - ], - "updated_at": "2019-01-22T19:32:47.232Z" - } - } -} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json deleted file mode 100644 index 0679717650af9..0000000000000 --- a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json +++ /dev/null @@ -1,435 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, - "mappings": { - "dynamic": "strict", - "properties": { - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "map": { - "properties": { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaceId": { - "type": "keyword" - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - } - } -} diff --git a/test/functional/fixtures/kbn_archiver/long_window_logstash_index_pattern.json b/test/functional/fixtures/kbn_archiver/long_window_logstash_index_pattern.json new file mode 100644 index 0000000000000..379e15fe0d7fe --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/long_window_logstash_index_pattern.json @@ -0,0 +1,31 @@ +{ + "attributes": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "long-window-logstash-*" + }, + "coreMigrationVersion": "8.4.0", + "id": "long-window-logstash-*", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzE3LDJd" +} + +{ + "attributes": { + "fields": "[]", + "timeFieldName": "@timestamp", + "title": "with-timefield" + }, + "coreMigrationVersion": "8.4.0", + "id": "with-timefield", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzE1LDJd" +} \ No newline at end of file diff --git a/test/functional/fixtures/kbn_archiver/management.json b/test/functional/fixtures/kbn_archiver/management.json new file mode 100644 index 0000000000000..11b9167e738d7 --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/management.json @@ -0,0 +1,42 @@ +{ + "attributes": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"expression script\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value\",\"lang\":\"expression\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.4.0", + "id": "f1e4c910-a2e6-11e7-bb30-233be9be6a15", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzIsMl0=" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Shared-Item Visualization AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{},\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.4.0", + "id": "Shared-Item-Visualization-AreaChart", + "migrationVersion": { + "visualization": "8.3.0" + }, + "references": [ + { + "id": "f1e4c910-a2e6-11e7-bb30-233be9be6a15", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzMsMl0=" +} \ No newline at end of file diff --git a/test/functional/fixtures/kbn_archiver/message_with_newline.json b/test/functional/fixtures/kbn_archiver/message_with_newline.json new file mode 100644 index 0000000000000..44e9d41ebf93e --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/message_with_newline.json @@ -0,0 +1,14 @@ +{ + "attributes": { + "fields": "[]", + "title": "newline-test" + }, + "coreMigrationVersion": "8.4.0", + "id": "newline-test", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzEzLDJd" +} \ No newline at end of file diff --git a/test/functional/fixtures/kbn_archiver/saved_objects_management/edit_saved_object.json b/test/functional/fixtures/kbn_archiver/saved_objects_management/edit_saved_object.json new file mode 100644 index 0000000000000..81269015a18d5 --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/saved_objects_management/edit_saved_object.json @@ -0,0 +1,74 @@ +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.4.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzQsMl0=" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true,\"legendDisplay\":\"show\",\"legendSize\":\"auto\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + }, + "coreMigrationVersion": "8.4.0", + "id": "75c3e060-1e7c-11e9-8488-65449e65d0ed", + "migrationVersion": { + "visualization": "8.3.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "version": "WzUsMl0=" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"}]", + "timeRestore": false, + "title": "A Dashboard", + "version": 1 + }, + "coreMigrationVersion": "8.4.0", + "id": "i-exist", + "migrationVersion": { + "dashboard": "8.3.0" + }, + "references": [ + { + "id": "75c3e060-1e7c-11e9-8488-65449e65d0ed", + "name": "1:panel_1", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z", + "version": "WzYsMl0=" +} \ No newline at end of file diff --git a/test/functional/fixtures/kbn_archiver/saved_objects_management/show_relationships.json b/test/functional/fixtures/kbn_archiver/saved_objects_management/show_relationships.json new file mode 100644 index 0000000000000..759add01f0a00 --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/saved_objects_management/show_relationships.json @@ -0,0 +1,34 @@ +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"}}" + }, + "optionsJSON": "{}", + "panelsJSON": "[]", + "timeRestore": false, + "title": "Dashboard with missing refs", + "version": 1 + }, + "coreMigrationVersion": "8.4.0", + "id": "dash-with-missing-refs", + "migrationVersion": { + "dashboard": "8.3.0" + }, + "references": [ + { + "id": "missing-vis-ref", + "name": "some missing ref", + "type": "visualization" + }, + { + "id": "missing-dashboard-ref", + "name": "some other missing ref", + "type": "dashboard" + } + ], + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z", + "version": "WzEsMl0=" +} \ No newline at end of file diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 218a1077d63ef..e8467ce714ff8 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -163,4 +163,28 @@ export class ConsolePageObject extends FtrService { return lines.length === 1 && text.trim() === ''; }); } + + public async selectAllRequests() { + const editor = await this.getEditorTextArea(); + const selectionKey = Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL']; + await editor.pressKeys([selectionKey, 'a']); + } + + public async hasSuccessBadge() { + try { + const responseEditor = await this.testSubjects.find('response-editor'); + return Boolean(await responseEditor.findByCssSelector('.ace_badge--success')); + } catch (e) { + return false; + } + } + + public async hasWarningBadge() { + try { + const responseEditor = await this.testSubjects.find('response-editor'); + return Boolean(await responseEditor.findByCssSelector('.ace_badge--warning')); + } catch (e) { + return false; + } + } } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 926bd5ab6b734..308ef6ace13ed 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -221,6 +221,17 @@ export class DataGridService extends FtrService { } await this.find.clickByButtonText('Remove column'); } + + public async clickCopyColumnValues(field: string) { + await this.openColMenuByField(field); + await this.find.clickByButtonText('Copy column'); + } + + public async clickCopyColumnName(field: string) { + await this.openColMenuByField(field); + await this.find.clickByButtonText('Copy name'); + } + public async getDetailsRow(): Promise { const detailRows = await this.getDetailsRows(); return detailRows[0]; diff --git a/test/plugin_functional/plugins/data_search/server/plugin.ts b/test/plugin_functional/plugins/data_search/server/plugin.ts index 5258211c544bb..b6e0208544995 100644 --- a/test/plugin_functional/plugins/data_search/server/plugin.ts +++ b/test/plugin_functional/plugins/data_search/server/plugin.ts @@ -55,7 +55,7 @@ export class DataSearchTestPlugin // Since the index pattern ID can change on each test run, we need // to look it up on the fly and insert it into the request. - const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory( + const indexPatterns = await data.indexPatterns.dataViewsServiceFactory( savedObjectsClient, clusterClient, req diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index 5e6836de2a986..111fbaec0e2f8 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -36,7 +36,7 @@ export class IndexPatternsTestPlugin async (context, req, res) => { const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( + const service = await data.indexPatterns.dataViewsServiceFactory( savedObjectsClient, elasticsearch.client.asScoped(req).asCurrentUser, req @@ -51,7 +51,7 @@ export class IndexPatternsTestPlugin async (context, req, res) => { const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( + const service = await data.indexPatterns.dataViewsServiceFactory( savedObjectsClient, elasticsearch.client.asScoped(req).asCurrentUser, req @@ -74,7 +74,7 @@ export class IndexPatternsTestPlugin const id = (req.params as Record).id; const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( + const service = await data.indexPatterns.dataViewsServiceFactory( savedObjectsClient, elasticsearch.client.asScoped(req).asCurrentUser, req @@ -97,7 +97,7 @@ export class IndexPatternsTestPlugin const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( + const service = await data.indexPatterns.dataViewsServiceFactory( savedObjectsClient, elasticsearch.client.asScoped(req).asCurrentUser, req @@ -121,7 +121,7 @@ export class IndexPatternsTestPlugin const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( + const service = await data.indexPatterns.dataViewsServiceFactory( savedObjectsClient, elasticsearch.client.asScoped(req).asCurrentUser, req diff --git a/tsconfig.base.json b/tsconfig.base.json index f263f7f9bced9..32bc0976bc0a2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -153,6 +153,8 @@ "@kbn/telemetry-management-section-plugin/*": ["src/plugins/telemetry_management_section/*"], "@kbn/telemetry-plugin": ["src/plugins/telemetry"], "@kbn/telemetry-plugin/*": ["src/plugins/telemetry/*"], + "@kbn/ui-actions-enhanced-plugin": ["src/plugins/ui_actions_enhanced"], + "@kbn/ui-actions-enhanced-plugin/*": ["src/plugins/ui_actions_enhanced/*"], "@kbn/ui-actions-plugin": ["src/plugins/ui_actions"], "@kbn/ui-actions-plugin/*": ["src/plugins/ui_actions/*"], "@kbn/unified-search-plugin": ["src/plugins/unified_search"], @@ -407,8 +409,6 @@ "@kbn/translations-plugin/*": ["x-pack/plugins/translations/*"], "@kbn/triggers-actions-ui-plugin": ["x-pack/plugins/triggers_actions_ui"], "@kbn/triggers-actions-ui-plugin/*": ["x-pack/plugins/triggers_actions_ui/*"], - "@kbn/ui-actions-enhanced-plugin": ["x-pack/plugins/ui_actions_enhanced"], - "@kbn/ui-actions-enhanced-plugin/*": ["x-pack/plugins/ui_actions_enhanced/*"], "@kbn/upgrade-assistant-plugin": ["x-pack/plugins/upgrade_assistant"], "@kbn/upgrade-assistant-plugin/*": ["x-pack/plugins/upgrade_assistant/*"], "@kbn/ux-plugin": ["x-pack/plugins/ux"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 3533ef314cacf..309322d349c27 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,7 +2,6 @@ "prefix": "xpack", "paths": { "xpack.actions": "plugins/actions", - "xpack.uiActionsEnhanced": "plugins/ui_actions_enhanced", "xpack.alerting": "plugins/alerting", "xpack.eventLog": "plugins/event_log", "xpack.stackAlerts": "plugins/stack_alerts", diff --git a/x-pack/examples/reporting_example/kibana.json b/x-pack/examples/reporting_example/kibana.json index 489b2bcd9f506..21329eba30a68 100644 --- a/x-pack/examples/reporting_example/kibana.json +++ b/x-pack/examples/reporting_example/kibana.json @@ -17,6 +17,5 @@ "navigation", "screenshotMode", "share" - ], - "requiredBundles": ["screenshotting"] + ] } diff --git a/x-pack/examples/reporting_example/public/containers/main.tsx b/x-pack/examples/reporting_example/public/containers/main.tsx index b6487c6130812..585a42cc6814a 100644 --- a/x-pack/examples/reporting_example/public/containers/main.tsx +++ b/x-pack/examples/reporting_example/public/containers/main.tsx @@ -40,7 +40,6 @@ import type { JobParamsPNGV2, } from '@kbn/reporting-plugin/common/types'; import type { ReportingStart } from '@kbn/reporting-plugin/public'; -import { LayoutTypes } from '@kbn/screenshotting-plugin/public'; import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common'; import { useApplicationContext } from '../application_context'; import { ROUTES } from '../constants'; @@ -85,9 +84,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp const getPDFJobParamsDefault = (): JobAppParamsPDF => { return { - layout: { - id: LayoutTypes.PRESERVE_LAYOUT, - }, + layout: { id: 'preserve_layout' }, relativeUrls: ['/app/reportingExample#/intended-visualization'], objectType: 'develeloperExample', title: 'Reporting Developer Example', @@ -97,9 +94,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp const getPDFJobParamsDefaultV2 = (): JobParamsPDFV2 => { return { version: '8.0.0', - layout: { - id: LayoutTypes.PRESERVE_LAYOUT, - }, + layout: { id: 'preserve_layout' }, locatorParams: [ { id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', params: { myTestState: {} } }, ], @@ -112,9 +107,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp const getPNGJobParamsDefaultV2 = (): JobParamsPNGV2 => { return { version: '8.0.0', - layout: { - id: LayoutTypes.PRESERVE_LAYOUT, - }, + layout: { id: 'preserve_layout' }, locatorParams: { id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', @@ -129,9 +122,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp const getCaptureTestPNGJobParams = (): JobParamsPNGV2 => { return { version: '8.0.0', - layout: { - id: LayoutTypes.PRESERVE_LAYOUT, - }, + layout: { id: 'preserve_layout' }, locatorParams: { id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', @@ -147,7 +138,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp return { version: '8.0.0', layout: { - id: print ? LayoutTypes.PRINT : LayoutTypes.PRESERVE_LAYOUT, + id: print ? 'print' : 'preserve_layout', dimensions: { // Magic numbers based on height of components not rendered on this screen :( height: 2400, diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/components/discover_drilldown_config/i18n.ts b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/components/discover_drilldown_config/i18n.ts index f61fffe8f8d56..2601cab20d9f2 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/components/discover_drilldown_config/i18n.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/components/discover_drilldown_config/i18n.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; export const txtChooseDestinationIndexPattern = i18n.translate( - 'xpack.uiActionsEnhanced.components.DiscoverDrilldownConfig.chooseIndexPattern', + 'uiActionsEnhanced.components.DiscoverDrilldownConfig.chooseIndexPattern', { defaultMessage: 'Choose destination index pattern', } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/i18n.ts b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/i18n.ts index de4b60f36c14d..95790845f9958 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/i18n.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/i18n.ts @@ -7,6 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const txtGoToDiscover = i18n.translate('xpack.uiActionsEnhanced.drilldown.goToDiscover', { +export const txtGoToDiscover = i18n.translate('uiActionsEnhanced.drilldown.goToDiscover', { defaultMessage: 'Go to Discover (example)', }); diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json index 0282ecbdd173a..0df8dda165f0d 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -19,8 +19,8 @@ { "path": "../../../src/plugins/discover/tsconfig.json" }, { "path": "../../../src/plugins/dashboard/tsconfig.json" }, { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../../../examples/developer_examples/tsconfig.json" }, { "path": "../../plugins/dashboard_enhanced/tsconfig.json" }, - { "path": "../../plugins/ui_actions_enhanced/tsconfig.json" }, ] } diff --git a/x-pack/plugins/apm/common/agent_configuration/all_option.ts b/x-pack/plugins/apm/common/agent_configuration/all_option.ts index 6737c8fa0c24c..6f5604989bba3 100644 --- a/x-pack/plugins/apm/common/agent_configuration/all_option.ts +++ b/x-pack/plugins/apm/common/agent_configuration/all_option.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; - export const ALL_OPTION_VALUE = 'ALL_OPTION_VALUE'; // human-readable label for service and environment. The "All" option should be translated. @@ -24,3 +23,8 @@ export function getOptionLabel(value: string | undefined) { export function omitAllOption(value?: string) { return value === ALL_OPTION_VALUE ? undefined : value; } + +export const ALL_OPTION = { + value: ALL_OPTION_VALUE, + label: getOptionLabel(ALL_OPTION_VALUE), +}; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts index 1314b757c66fb..4661ea67ae2ab 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/agent_configurations.spec.ts @@ -101,4 +101,29 @@ describe('Agent configuration', () => { cy.wait('@serviceEnvironmentApi'); cy.contains('production'); }); + it('displays All label when selecting all option', () => { + cy.intercept( + 'GET', + '/api/apm/settings/agent-configuration/environments' + ).as('serviceEnvironmentApi'); + cy.contains('Create configuration').click(); + cy.get('[data-test-subj="serviceNameComboBox"]') + .click() + .type('All') + .type('{enter}'); + cy.contains('All').realClick(); + cy.wait('@serviceEnvironmentApi'); + + cy.get('[data-test-subj="serviceEnviromentComboBox"]') + .click({ force: true }) + .type('All'); + + cy.get('mark').contains('All').click(); + cy.contains('Next step').click(); + cy.contains('Service name All'); + cy.contains('Environment All'); + cy.contains('Edit').click(); + cy.wait('@serviceEnvironmentApi'); + cy.contains('All'); + }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx index 5e7e376334d31..c92bd14607677 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx @@ -51,7 +51,7 @@ export function ServiceGroupsList() { const sortedItems = sortBy(filteredItems, (item) => apmServiceGroupsSortType === 'alphabetical' - ? item.groupName + ? item.groupName.toLowerCase() : item.updatedAt ); diff --git a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx index 2fb481b26be2e..41e22ac840bc1 100644 --- a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx @@ -9,7 +9,10 @@ import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { SuggestionsSelect } from '../../../../../shared/suggestions_select'; -import { ENVIRONMENT_ALL } from '../../../../../../../common/environment_filter_values'; +import { + getOptionLabel, + ALL_OPTION, +} from '../../../../../../../common/agent_configuration/all_option'; interface Props { title: string; @@ -40,8 +43,8 @@ export function FormRowSuggestionsSelect({ > type === 'apm' ); onChange({ - isValid: validateVersion(runtimeAttachmentSettings.version), + isValid: + !runtimeAttachmentSettings.enabled || + validateVersion(runtimeAttachmentSettings.version), updatedPolicy: { ...newPolicy, inputs: [ diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index ab92e2b5a3a29..e97a2799d74c1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -93,20 +93,12 @@ export function TimeseriesChart({ query: { comparisonEnabled, offset }, } = useAnyOfApmParams('/services', '/backends/*', '/services/{serviceName}'); - const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x)); - - const timeZone = getTimeZone(core.uiSettings); - - const min = Math.min(...xValues); - const max = Math.max(...xValues); - const anomalyChartTimeseries = getChartAnomalyTimeseries({ anomalyTimeseries, theme, anomalyTimeseriesColor: anomalyTimeseries?.color, }); - const xFormatter = niceTimeFormatter([min, max]); const isEmpty = isTimeseriesEmpty(timeseries); const annotationColor = theme.eui.euiColorSuccess; @@ -114,23 +106,47 @@ export function TimeseriesChart({ comparisonEnabled && isExpectedBoundsComparison(offset); const allSeries = [ ...timeseries, - ...(isComparingExpectedBounds && anomalyChartTimeseries?.boundaries - ? anomalyChartTimeseries?.boundaries + ...(isComparingExpectedBounds + ? anomalyChartTimeseries?.boundaries ?? [] : []), ...(anomalyChartTimeseries?.scores ?? []), - ]; + ] + // Sorting series so that area type series are before line series + // This is a workaround so that the legendSort works correctly + // Can be removed when https://github.com/elastic/elastic-charts/issues/1685 is resolved + .sort( + isComparingExpectedBounds + ? (prev, curr) => prev.type.localeCompare(curr.type) + : undefined + ); + + const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x)); + const xValuesExpectedBounds = + anomalyChartTimeseries?.boundaries?.flatMap(({ data }) => + data.map(({ x }) => x) + ) ?? []; + + const timeZone = getTimeZone(core.uiSettings); + + const min = Math.min(...xValues); + const max = Math.max(...xValues, ...xValuesExpectedBounds); + const xFormatter = niceTimeFormatter([min, max]); + const xDomain = isEmpty ? { min: 0, max: 1 } : { min, max }; - const legendSort = (a: SeriesIdentifier, b: SeriesIdentifier) => { - // Using custom legendSort here when comparing expected bounds - // because by default elastic-charts will show legends for expected bounds first - // but for consistency, we are making `Expected bounds` last - if ((a as XYChartSeriesIdentifier)?.specId === expectedBoundsTitle) - return -1; - if ((b as XYChartSeriesIdentifier)?.specId === expectedBoundsTitle) - return -1; - return 1; - }; + // Using custom legendSort here when comparing expected bounds + // because by default elastic-charts will show legends for expected bounds first + // but for consistency, we are making `Expected bounds` last + // See https://github.com/elastic/elastic-charts/issues/1685 + const legendSort = isComparingExpectedBounds + ? (a: SeriesIdentifier, b: SeriesIdentifier) => { + if ((a as XYChartSeriesIdentifier)?.specId === expectedBoundsTitle) + return -1; + if ((b as XYChartSeriesIdentifier)?.specId === expectedBoundsTitle) + return -1; + return 1; + } + : undefined; return ( { diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx index 17eded47f50fe..7a335964d5ccc 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx @@ -11,8 +11,8 @@ import { uniqueId } from 'lodash'; import React, { useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { QuerySuggestion } from '@kbn/unified-search-plugin/public'; -import { DataView } from '@kbn/data-plugin/common'; import { esKuery } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_options.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_options.ts index 062a330b67f05..0eff4bc9b591f 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_options.ts +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_options.ts @@ -110,12 +110,12 @@ function getSelectOptions({ export function getComparisonOptions({ start, end, - canGetJobs, + showSelectedBoundsOption, anomalyDetectionJobsStatus, anomalyDetectionJobsData, preferredEnvironment, }: { - canGetJobs?: boolean; + showSelectedBoundsOption?: boolean; anomalyDetectionJobsStatus?: AnomalyDetectionJobsContextValue['anomalyDetectionJobsStatus']; anomalyDetectionJobsData?: AnomalyDetectionJobsContextValue['anomalyDetectionJobsData']; preferredEnvironment?: Environment; @@ -144,7 +144,6 @@ export function getComparisonOptions({ } const hasMLJobsMatchingEnv = - canGetJobs && Array.isArray(anomalyDetectionJobsData?.jobs) && anomalyDetectionJobsData?.jobs.some( (j) => j.environment === preferredEnvironment @@ -157,7 +156,7 @@ export function getComparisonOptions({ msDiff, }); - if (canGetJobs) { + if (showSelectedBoundsOption) { const disabled = anomalyDetectionJobsStatus === 'success' && !hasMLJobsMatchingEnv; comparisonOptions.push({ diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index bff057ea47f71..8b49fe388bcf2 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -8,9 +8,10 @@ import { EuiCheckbox, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { useUiTracker } from '@kbn/observability-plugin/public'; +import { useApmRouter } from '../../../hooks/use_apm_router'; import { useEnvironmentsContext } from '../../../context/environments_context/use_environments_context'; import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; @@ -40,6 +41,9 @@ export function TimeComparison() { query: { rangeFrom, rangeTo, comparisonEnabled, offset }, } = useAnyOfApmParams('/services', '/backends/*', '/services/{serviceName}'); + const location = useLocation(); + const apmRouter = useApmRouter(); + const { anomalyDetectionJobsStatus, anomalyDetectionJobsData } = useAnomalyDetectionJobsContext(); const { core } = useApmPluginContext(); @@ -48,11 +52,20 @@ export function TimeComparison() { const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; + const comparisonOptions = useMemo(() => { + const matchingRoutes = apmRouter.getRoutesToMatch(location.pathname); + // Only show the "Expected bounds" option in Overview and Transactions tabs + const showExpectedBoundsForThisTab = matchingRoutes.some( + (d) => + d.path === '/services/{serviceName}/overview' || + d.path === '/services/{serviceName}/transactions' + ); + const timeComparisonOptions = getComparisonOptions({ start, end, - canGetJobs, + showSelectedBoundsOption: showExpectedBoundsForThisTab && canGetJobs, anomalyDetectionJobsStatus, anomalyDetectionJobsData, preferredEnvironment, @@ -66,6 +79,8 @@ export function TimeComparison() { start, end, preferredEnvironment, + apmRouter, + location.pathname, ]); const isSelectedComparisonTypeAvailable = comparisonOptions.some( diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts index c86ef3962b428..b1c77a4fe4372 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts @@ -24,6 +24,27 @@ import { anomalySearch } from './anomaly_search'; import { getAnomalyResultBucketSize } from './get_anomaly_result_bucket_size'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; +const FALLBACK_ML_BUCKET_SPAN = 15; // minutes + +function divide(value: number | null, divider: number) { + if (value === null) { + return null; + } + return value / divider; +} + +// Expected bounds are retrieved with bucket span interval padded to the time range +// so we need to cut the excess bounds to just the start and end time +// so that the chart show up correctly without the padded time +function getBoundedX(value: number | null, start: number, end: number) { + if (value === null) { + return null; + } + if (value < start) return start; + if (value > end) return end; + return value; +} + export async function getAnomalyTimeseries({ serviceName, transactionType, @@ -58,18 +79,24 @@ export async function getAnomalyTimeseries({ j.datafeedState !== undefined && j.environment === preferredEnvironment )?.bucketSpan; - // If the calculated bucketSize is smaller than the bucket span interval, - // use the original job's bucket_span - const minBucketSize = preferredBucketSpan - ? parseInterval(preferredBucketSpan)?.asSeconds() - : undefined; + const minBucketSize = + parseInterval( + preferredBucketSpan ?? `${FALLBACK_ML_BUCKET_SPAN}m` + )?.asSeconds() ?? FALLBACK_ML_BUCKET_SPAN * 60; // secs + + // Expected bounds (aka ML model plots) are stored as points in time, in intervals of the predefined bucket_span, + // so to query bounds that include start and end time + // we need to append bucket size before and after the range + const extendedStart = start - minBucketSize * 1000; // ms + const extendedEnd = end + minBucketSize * 1000; // ms const { intervalString } = getAnomalyResultBucketSize({ - start, - end, + start: extendedStart, + end: extendedEnd, + // If the calculated bucketSize is smaller than the bucket span interval, + // use the original job's bucket_span minBucketSize, }); - const anomaliesResponse = await anomalySearch( mlSetup.mlSystem.mlAnomalySearch, { @@ -82,7 +109,7 @@ export async function getAnomalyTimeseries({ serviceName, transactionType, }), - ...rangeQuery(start, end, 'timestamp'), + ...rangeQuery(extendedStart, extendedEnd, 'timestamp'), ...apmMlJobsQuery(mlJobs), ], }, @@ -128,8 +155,8 @@ export async function getAnomalyTimeseries({ field: 'timestamp', fixed_interval: intervalString, extended_bounds: { - min: start, - max: end, + min: extendedStart, + max: extendedEnd, }, }, aggs: { @@ -166,13 +193,6 @@ export async function getAnomalyTimeseries({ const jobsById = keyBy(mlJobs, (job) => job.jobId); - function divide(value: number | null, divider: number) { - if (value === null) { - return null; - } - return value / divider; - } - const series: Array = anomaliesResponse.aggregations?.by_timeseries_id.buckets.map((bucket) => { const jobId = bucket.key.jobId as string; @@ -210,11 +230,13 @@ export async function getAnomalyTimeseries({ divider ), })), - bounds: bucket.timeseries.buckets.map((dateBucket) => ({ - x: dateBucket.key as number, - y0: divide(dateBucket.model_lower.value, divider), - y1: divide(dateBucket.model_upper.value, divider), - })), + bounds: bucket.timeseries.buckets.map((dateBucket) => { + return { + x: getBoundedX(dateBucket.key, start, end) as number, + y0: divide(dateBucket.model_lower.value, divider), + y1: divide(dateBucket.model_upper.value, divider), + }; + }), }; }) ?? []; diff --git a/x-pack/plugins/apm/server/routes/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/routes/rum_client/__snapshots__/queries.test.ts.snap index 6adecb0582ee1..bad786579a605 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/routes/rum_client/__snapshots__/queries.test.ts.snap @@ -73,102 +73,6 @@ Object { } `; -exports[`rum client dashboard queries fetches js errors 1`] = ` -Object { - "apm": Object { - "events": Array [ - "error", - ], - }, - "body": Object { - "aggs": Object { - "errors": Object { - "aggs": Object { - "bucket_truncate": Object { - "bucket_sort": Object { - "from": 0, - "size": 5, - }, - }, - "impactedPages": Object { - "aggs": Object { - "pageCount": Object { - "cardinality": Object { - "field": "transaction.id", - }, - }, - }, - "filter": Object { - "term": Object { - "transaction.type": "page-load", - }, - }, - }, - "sample": Object { - "top_hits": Object { - "_source": Array [ - "error.exception.message", - "error.exception.type", - "error.grouping_key", - "@timestamp", - ], - "size": 1, - "sort": Array [ - Object { - "@timestamp": "desc", - }, - ], - }, - }, - }, - "terms": Object { - "field": "error.grouping_key", - "size": 500, - }, - }, - "totalErrorGroups": Object { - "cardinality": Object { - "field": "error.grouping_key", - }, - }, - "totalErrorPages": Object { - "cardinality": Object { - "field": "transaction.id", - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 0, - "lte": 50000, - }, - }, - }, - Object { - "term": Object { - "agent.name": "rum-js", - }, - }, - Object { - "term": Object { - "service.language.name": "javascript", - }, - }, - ], - "must_not": Array [], - }, - }, - "size": 0, - "track_total_hits": true, - }, -} -`; - exports[`rum client dashboard queries fetches long task metrics 1`] = ` Object { "apm": Object { diff --git a/x-pack/plugins/apm/server/routes/rum_client/queries.test.ts b/x-pack/plugins/apm/server/routes/rum_client/queries.test.ts index 12baa775a920c..9fe54ac1f7701 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/queries.test.ts +++ b/x-pack/plugins/apm/server/routes/rum_client/queries.test.ts @@ -14,7 +14,6 @@ import { getPageViewTrends } from './get_page_view_trends'; import { getPageLoadDistribution } from './get_page_load_distribution'; import { getLongTaskMetrics } from './get_long_task_metrics'; import { getWebCoreVitals } from './get_web_core_vitals'; -import { getJSErrors } from './get_js_errors'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; describe('rum client dashboard queries', () => { @@ -90,17 +89,4 @@ describe('rum client dashboard queries', () => { ); expect(mock.params).toMatchSnapshot(); }); - - it('fetches js errors', async () => { - mock = await inspectSearchParams((setup) => - getJSErrors({ - setup, - pageSize: 5, - pageIndex: 0, - start: 0, - end: 50000, - }) - ); - expect(mock.params).toMatchSnapshot(); - }); }); diff --git a/x-pack/plugins/apm/server/routes/rum_client/route.ts b/x-pack/plugins/apm/server/routes/rum_client/route.ts index 8807d16afbb66..c15c07b322997 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/route.ts +++ b/x-pack/plugins/apm/server/routes/rum_client/route.ts @@ -9,7 +9,6 @@ import { Logger } from '@kbn/core/server'; import { isoToEpochRt } from '@kbn/io-ts-utils'; import { setupRequest, Setup } from '../../lib/helpers/setup_request'; import { getClientMetrics } from './get_client_metrics'; -import { getJSErrors } from './get_js_errors'; import { getLongTaskMetrics } from './get_long_task_metrics'; import { getPageLoadDistribution } from './get_page_load_distribution'; import { getPageViewTrends } from './get_page_view_trends'; @@ -279,48 +278,6 @@ const rumLongTaskMetrics = createApmServerRoute({ }, }); -const rumJSErrors = createApmServerRoute({ - endpoint: 'GET /internal/apm/ux/js-errors', - params: t.type({ - query: t.intersection([ - uiFiltersRt, - rangeRt, - t.type({ pageSize: t.string, pageIndex: t.string }), - t.partial({ urlQuery: t.string }), - ]), - }), - options: { tags: ['access:apm'] }, - handler: async ( - resources - ): Promise<{ - totalErrorPages: number; - totalErrors: number; - totalErrorGroups: number; - items: - | Array<{ - count: number; - errorGroupId: string | number; - errorMessage: string; - }> - | undefined; - }> => { - const setup = await setupUXRequest(resources); - - const { - query: { pageSize, pageIndex, urlQuery, start, end }, - } = resources.params; - - return getJSErrors({ - setup, - urlQuery, - pageSize: Number(pageSize), - pageIndex: Number(pageIndex), - start, - end, - }); - }, -}); - const rumHasDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview/has_rum_data', params: t.partial({ @@ -383,6 +340,5 @@ export const rumRouteRepository = { ...rumVisitorsBreakdownRoute, ...rumWebCoreVitals, ...rumLongTaskMetrics, - ...rumJSErrors, ...rumHasDataRoute, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts index 5fcd5e93e805a..63477a325824b 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { RedirectOptions } from '@kbn/share-plugin/public'; import { JobAppParamsPDFV2 } from '@kbn/reporting-plugin/common/types'; +import type { RedirectOptions } from '@kbn/share-plugin/public'; import { CanvasAppLocatorParams, CANVAS_APP_LOCATOR } from '../../../../common/locator'; import { CanvasWorkpad } from '../../../../types'; @@ -45,10 +45,7 @@ export function getPdfJobParams( } return { - layout: { - dimensions: { width, height }, - id: 'canvas', - }, + layout: { dimensions: { width, height }, id: 'canvas' }, objectType: 'canvas workpad', locatorParams, title, diff --git a/x-pack/plugins/canvas/public/lib/es_service.ts b/x-pack/plugins/canvas/public/lib/es_service.ts index 7b86716aef7e5..9d558243c9421 100644 --- a/x-pack/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/plugins/canvas/public/lib/es_service.ts @@ -6,7 +6,6 @@ */ // TODO - clint: convert to service abstraction -import { IndexPatternAttributes } from '@kbn/data-plugin/public'; import { API_ROUTE } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; @@ -49,7 +48,7 @@ export const getFields = (index = '_all') => { export const getIndices = () => getSavedObjectsClient() - .find({ + .find<{ title: string }>({ type: 'index-pattern', fields: ['title'], searchFields: ['title'], @@ -70,7 +69,7 @@ export const getDefaultIndex = () => { return defaultIndexId ? getSavedObjectsClient() - .get('index-pattern', defaultIndexId) + .get<{ title: string }>('index-pattern', defaultIndexId) .then((defaultIndex) => defaultIndex.attributes.title) .catch((err) => { const notifyService = pluginServices.getServices().notify; diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 908428673c30d..98eb504ff7094 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 ddb8ba7d89ea3..37e1173c48052 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,25 +328,25 @@ "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" + "example": "A case description." }, "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.", + "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. If the case was closed after less than half a second, the duration is rounded down to zero.\n", "example": 120 }, "external_service": { @@ -417,14 +417,12 @@ "type": "string" }, "example": [ - "phishing", - "social engineering", - "bubblegum" + "tag-1" ] }, "title": { "type": "string", - "example": "This case will self-destruct in 5 seconds" + "example": "Case title 1" }, "totalAlerts": { "type": "integer", @@ -604,25 +602,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 +790,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,25 +817,25 @@ "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" + "example": "A case description." }, "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.", + "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. If the case was closed after less than half a second, the duration is rounded down to zero.\n", "example": 120 }, "external_service": { @@ -908,14 +906,12 @@ "type": "string" }, "example": [ - "phishing", - "social engineering", - "bubblegum" + "tag-1" ] }, "title": { "type": "string", - "example": "This case will self-destruct in 5 seconds" + "example": "Case title 1" }, "totalAlerts": { "type": "integer", @@ -1011,7 +1007,7 @@ "type": "string" }, "example": "now-1d", - "x-preview": true + "x-technical-preview": true }, { "$ref": "#/components/parameters/owner" @@ -1143,7 +1139,7 @@ } ] }, - "example": "phishing" + "example": "tag-1" }, { "name": "to", @@ -1153,7 +1149,7 @@ "type": "string" }, "example": "now%2B1d", - "x-preview": true + "x-technical-preview": true } ], "responses": { @@ -1270,20 +1266,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,25 +1293,25 @@ "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" + "example": "A case description." }, "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.", + "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. If the case was closed after less than half a second, the duration is rounded down to zero.\n", "example": 120 }, "external_service": { @@ -1389,14 +1382,12 @@ "type": "string" }, "example": [ - "phishing", - "social engineering", - "bubblegum" + "tag-1" ] }, "title": { "type": "string", - "example": "This case will self-destruct in 5 seconds" + "example": "Case title 1" }, "totalAlerts": { "type": "integer", @@ -1476,19 +1467,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 +1751,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 +1827,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 +1859,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 +1883,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 +2073,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 +2091,7 @@ "$ref": "#/components/parameters/kbn_xsrf" }, { - "$ref": "#/components/parameters/space_id" + "$ref": "#/components/parameters/configuration_id" } ], "requestBody": { @@ -1982,156 +2100,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" - } - }, - "required": [ - "id", - "version" - ] - } + }, + "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.\n", + "type": "string", + "example": "WzIwMiwxXQ==" } - } - }, - "examples": { - "updateCaseRequest": { - "$ref": "#/components/examples/update_case_request" - } + }, + "required": [ + "version" + ] } } } @@ -2142,277 +2218,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" - }, - "priority": { - "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", - "type": "string" + "action_type": { + "type": "string", + "example": "overwrite" }, - "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 +2414,2396 @@ } ] }, - "/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.", - "schema": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" + "$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": "A case description." + }, + "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. If the case was closed after less than half a second, the duration is rounded down to zero.\n", + "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": [ + "tag-1" + ] + }, + "title": { + "type": "string", + "example": "Case title 1" + }, + "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": "A case description." + }, + "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. If the case was closed after less than half a second, the duration is rounded down to zero.\n", + "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": [ + "tag-1" + ] + }, + "title": { + "type": "string", + "example": "Case title 1" + }, + "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": "tag-1" + }, + { + "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": "A case description." + }, + "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. If the case was closed after less than half a second, the duration is rounded down to zero.\n", + "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": [ + "tag-1" + ] + }, + "title": { + "type": "string", + "example": "Case title 1" + }, + "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": "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": [ + { + "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" + }, + { + "$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=" + } + } } } - ] + } } - }, - { - "$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" - }, + } + }, + "servers": [ { - "name": "sortOrder", - "in": "query", - "description": "Determines the sort order.", - "schema": { - "type": "string", - "enum": [ - "asc", - "desc" - ], - "default": "desc" - }, - "example": "asc" - }, + "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" }, - "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" - } - } + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" }, - "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" + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" }, - "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 + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" }, - "external_service": { + "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.\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" + "example": "none" }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "phishing", - "social engineering", - "bubblegum" - ] - }, - "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 +4846,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 +4876,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 +4918,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,17 +4950,25 @@ "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.", "value": { - "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", - "title": "This case will self-destruct in 5 seconds", + "description": "A case description.", + "title": "Case title 1", "tags": [ - "phishing", - "social engineering" + "tag-1" ], "connector": { "id": "131d4448-abe0-4789-939d-8ef60680b498", @@ -3065,7 +4983,7 @@ "settings": { "syncAlerts": true }, - "owner": "securitySolution" + "owner": "cases" } }, "create_case_response": { @@ -3076,26 +4994,24 @@ "comments": [], "totalComment": 0, "totalAlerts": 0, - "title": "This case will self-destruct in 5 seconds", + "title": "Case title 1", "tags": [ - "phishing", - "social engineering", - "bubblegum" + "tag-1" ], "settings": { "syncAlerts": true }, - "owner": "securitySolution", - "description": "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", + "owner": "cases", + "description": "A case description.", "duration": null, "severity": "low", "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", "created_by": { - "email": "ahunley@imf.usa.gov", - "full_name": "Alan Hunley", - "username": "ahunley" + "email": null, + "full_name": null, + "username": "elastic" }, "status": "open", "updated_at": null, @@ -3130,11 +5046,9 @@ "parent": null } }, - "description": "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!", + "description": "A case description.", "tags": [ - "phishing", - "social engineering", - "bubblegum" + "tag-1" ], "settings": { "syncAlerts": true @@ -3152,33 +5066,31 @@ "comments": [], "totalComment": 0, "totalAlerts": 0, - "title": "This case will self-destruct in 5 seconds", + "title": "Case title 1", "tags": [ - "phishing", - "social engineering", - "bubblegum" + "tag-1" ], "settings": { "syncAlerts": true }, - "owner": "securitySolution", - "description": "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!", + "owner": "cases", + "description": "A case description.", "duration": null, "severity": "low", "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", "created_by": { - "email": "ahunley@imf.usa.gov", - "full_name": "Alan Hunley", - "username": "ahunley" + "email": null, + "full_name": null, + "username": "elastic" }, "status": "open", "updated_at": "2022-05-13T09:48:33.043Z", "updated_by": { - "email": "classified@hms.oo.gov.uk", - "full_name": "Classified", - "username": "M" + "email": null, + "full_name": null, + "username": "elastic" }, "connector": { "id": "131d4448-abe0-4789-939d-8ef60680b498", @@ -3193,9 +5105,9 @@ "external_service": { "external_title": "IS-4", "pushed_by": { - "full_name": "Classified", - "email": "classified@hms.oo.gov.uk", - "username": "M" + "full_name": null, + "email": null, + "username": "elastic" }, "external_url": "https://hms.atlassian.net/browse/IS-4", "pushed_at": "2022-05-13T09:20:40.672Z", @@ -3207,7 +5119,7 @@ ] }, "find_case_response": { - "summary": "Retrieve the first five cases with the `phishing` tag, in ascending order by last update time.", + "summary": "Retrieve the first five cases with the `tag-1` tag, in ascending order by last update time.", "value": { "page": 1, "per_page": 5, @@ -3221,29 +5133,29 @@ "totalAlerts": 0, "title": "Case title", "tags": [ - "phishing" + "tag-1" ], "description": "Case description", "settings": { "syncAlerts": true }, - "owner": "securitySolution", + "owner": "cases", "duration": null, "severity": "low", "closed_at": null, "closed_by": null, "created_at": "2022-05-12T00:16:36.371Z", "created_by": { - "email": "jdoe@email.com", - "full_name": "Jane Doe", - "username": "jdoe" + "email": null, + "full_name": null, + "username": "elastic" }, "status": "open", "updated_at": "2022-05-12T00:27:58.162Z", "updated_by": { - "email": "jsmith@email.com", - "full_name": "Joe Smith", - "username": "jsmith" + "email": null, + "full_name": null, + "username": "elastic" }, "connector": { "id": "none", diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index dbf11fc42e892..bbdadd3b621cf 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,26 +307,23 @@ 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: >- - 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 + example: A case description. duration: type: integer - description: >- + 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. + duration is set to null. If the case was closed after less + than half a second, the duration is rounded down to zero. example: 120 external_service: type: object @@ -374,12 +372,10 @@ paths: items: type: string example: - - phishing - - social engineering - - bubblegum + - tag-1 title: type: string - example: This case will self-destruct in 5 seconds + example: Case title 1 totalAlerts: type: integer example: 0 @@ -543,23 +539,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 +704,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,26 +728,23 @@ 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: >- - 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 + example: A case description. duration: type: integer - description: >- + 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. + duration is set to null. If the case was closed after less + than half a second, the duration is rounded down to zero. example: 120 external_service: type: object @@ -799,12 +793,10 @@ paths: items: type: string example: - - phishing - - social engineering - - bubblegum + - tag-1 title: type: string - example: This case will self-destruct in 5 seconds + example: Case title 1 totalAlerts: type: integer example: 0 @@ -874,7 +866,7 @@ paths: schema: type: string example: now-1d - x-preview: true + x-technical-preview: true - $ref: '#/components/parameters/owner' - name: page in: query @@ -956,7 +948,7 @@ paths: - type: array items: type: string - example: phishing + example: tag-1 - name: to in: query description: >- @@ -965,7 +957,7 @@ paths: schema: type: string example: now%2B1d - x-preview: true + x-technical-preview: true responses: '200': description: Indicates a successful call. @@ -1086,21 +1078,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,26 +1102,24 @@ 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: >- - 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 + example: A case description. duration: type: integer - description: >- + 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. + closed, the duration is set to null. If the case was + closed after less than half a second, the duration + is rounded down to zero. example: 120 external_service: type: object @@ -1180,12 +1168,10 @@ paths: items: type: string example: - - phishing - - social engineering - - bubblegum + - tag-1 title: type: string - example: This case will self-destruct in 5 seconds + example: Case title 1 totalAlerts: type: integer example: 0 @@ -1230,25 +1216,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 +1547,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,486 +1576,685 @@ 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. - 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: + 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: - 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. - 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 - required: - - id - - version - examples: - updateCaseRequest: - $ref: '#/components/examples/update_case_request' + 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: 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 + 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: + 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: >- @@ -1921,21 +2335,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: @@ -1947,26 +2359,23 @@ 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: >- - 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 + example: A case description. duration: type: integer - description: >- + 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. + duration is set to null. If the case was closed after less + than half a second, the duration is rounded down to zero. example: 120 external_service: type: object @@ -2015,12 +2424,10 @@ paths: items: type: string example: - - phishing - - social engineering - - bubblegum + - tag-1 title: type: string - example: This case will self-destruct in 5 seconds + example: Case title 1 totalAlerts: type: integer example: 0 @@ -2047,404 +2454,1677 @@ paths: type: string example: WzUzMiwxXQ== examples: - updateCaseResponse: - $ref: '#/components/examples/update_case_response' + 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: A case description. + 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. If the case was closed after less + than half a second, the duration is rounded down to zero. + 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: + - tag-1 + title: + type: string + example: Case title 1 + 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: tag-1 + - 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: A case description. + 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. If the case was + closed after less than half a second, the duration + is rounded down to zero. + 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: + - tag-1 + title: + type: string + example: Case title 1 + 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 + 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/_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 + 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 - closed_by: type: object properties: - email: - type: string - full_name: - type: string - username: + caseId: + description: The case identifier for Swimlane connectors. type: string - nullable: true - example: null - comments: - type: array - items: - type: string - example: [] - connector: - type: object - properties: - fields: + 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 +4148,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 +4170,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 +4205,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,17 +4232,24 @@ 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. value: - description: >- - James Bond clicked on a highly suspicious email banner advertising - cheap holidays for underpaid civil servants. - title: This case will self-destruct in 5 seconds + description: A case description. + title: Case title 1 tags: - - phishing - - social engineering + - tag-1 connector: id: 131d4448-abe0-4789-939d-8ef60680b498 name: My connector @@ -2553,7 +4260,7 @@ components: parent: null settings: syncAlerts: true - owner: securitySolution + owner: cases create_case_response: summary: >- The create case API returns a JSON object that includes the user who @@ -2564,27 +4271,22 @@ components: comments: [] totalComment: 0 totalAlerts: 0 - title: This case will self-destruct in 5 seconds + title: Case title 1 tags: - - phishing - - social engineering - - bubblegum + - tag-1 settings: syncAlerts: true - owner: securitySolution - description: >- - 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 + owner: cases + description: A case description. duration: null severity: low closed_at: null closed_by: null created_at: '2022-05-13T09:16:17.416Z' created_by: - email: ahunley@imf.usa.gov - full_name: Alan Hunley - username: ahunley + email: null + full_name: null + username: elastic status: open updated_at: null updated_by: null @@ -2611,14 +4313,9 @@ components: issueType: '10006' priority: null parent: null - description: >- - 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! + description: A case description. tags: - - phishing - - social engineering - - bubblegum + - tag-1 settings: syncAlerts: true update_case_response: @@ -2631,33 +4328,28 @@ components: comments: [] totalComment: 0 totalAlerts: 0 - title: This case will self-destruct in 5 seconds + title: Case title 1 tags: - - phishing - - social engineering - - bubblegum + - tag-1 settings: syncAlerts: true - owner: securitySolution - description: >- - 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! + owner: cases + description: A case description. duration: null severity: low closed_at: null closed_by: null created_at: '2022-05-13T09:16:17.416Z' created_by: - email: ahunley@imf.usa.gov - full_name: Alan Hunley - username: ahunley + email: null + full_name: null + username: elastic status: open updated_at: '2022-05-13T09:48:33.043Z' updated_by: - email: classified@hms.oo.gov.uk - full_name: Classified - username: M + email: null + full_name: null + username: elastic connector: id: 131d4448-abe0-4789-939d-8ef60680b498 name: My connector @@ -2669,9 +4361,9 @@ components: external_service: external_title: IS-4 pushed_by: - full_name: Classified - email: classified@hms.oo.gov.uk - username: M + full_name: null + email: null + username: elastic external_url: https://hms.atlassian.net/browse/IS-4 pushed_at: '2022-05-13T09:20:40.672Z' connector_id: 05da469f-1fde-4058-99a3-91e4807e2de8 @@ -2679,8 +4371,8 @@ components: connector_name: Jira find_case_response: summary: >- - Retrieve the first five cases with the `phishing` tag, in ascending - order by last update time. + Retrieve the first five cases with the `tag-1` tag, in ascending order + by last update time. value: page: 1 per_page: 5 @@ -2693,26 +4385,26 @@ components: totalAlerts: 0 title: Case title tags: - - phishing + - tag-1 description: Case description settings: syncAlerts: true - owner: securitySolution + owner: cases duration: null severity: low closed_at: null closed_by: null created_at: '2022-05-12T00:16:36.371Z' created_by: - email: jdoe@email.com - full_name: Jane Doe - username: jdoe + email: null + full_name: null + username: elastic status: open updated_at: '2022-05-12T00:27:58.162Z' updated_by: - email: jsmith@email.com - full_name: Joe Smith - username: jsmith + email: null + full_name: null + username: elastic connector: id: none name: none diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml index 0659ed18a8569..0e5b6c01aa3b6 100644 --- a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml @@ -1,9 +1,9 @@ summary: Create a security case that uses a Jira connector. value: { - "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", - "title": "This case will self-destruct in 5 seconds", - "tags": [ "phishing","social engineering"], + "description": "A case description.", + "title": "Case title 1", + "tags": [ "tag-1" ], "connector": { "id": "131d4448-abe0-4789-939d-8ef60680b498", "name": "My connector", @@ -17,5 +17,5 @@ value: "settings": { "syncAlerts": true }, - "owner": "securitySolution" + "owner": "cases" } diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml index 9646425bca0fe..c05f7bedd6599 100644 --- a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml @@ -6,26 +6,22 @@ value: "comments": [], "totalComment": 0, "totalAlerts": 0, - "title": "This case will self-destruct in 5 seconds", - "tags": [ - "phishing", - "social engineering", - "bubblegum" - ], + "title": "Case title 1", + "tags": [ "tag-1" ], "settings": { "syncAlerts": true }, - "owner": "securitySolution", - "description": "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", + "owner": "cases", + "description": "A case description.", "duration": null, "severity": "low", "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", "created_by": { - "email": "ahunley@imf.usa.gov", - "full_name": "Alan Hunley", - "username": "ahunley" + "email": null, + "full_name": null, + "username": "elastic" }, "status": "open", "updated_at": null, diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/find_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/find_case_response.yaml index 2603d25cce6ac..1c8168dde7708 100644 --- a/x-pack/plugins/cases/docs/openapi/components/examples/find_case_response.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/examples/find_case_response.yaml @@ -1,4 +1,4 @@ -summary: Retrieve the first five cases with the `phishing` tag, in ascending order by last update time. +summary: Retrieve the first five cases with the `tag-1` tag, in ascending order by last update time. value: { "page": 1, @@ -12,26 +12,26 @@ value: "totalComment": 1, "totalAlerts": 0, "title": "Case title", - "tags": [ "phishing" ], + "tags": [ "tag-1" ], "description": "Case description", "settings": { "syncAlerts": true }, - "owner": "securitySolution", + "owner": "cases", "duration": null, "severity": "low", "closed_at": null, "closed_by": null, "created_at": "2022-05-12T00:16:36.371Z", "created_by": { - "email": "jdoe@email.com", - "full_name": "Jane Doe", - "username": "jdoe" + "email": null, + "full_name": null, + "username": "elastic" }, "status": "open", "updated_at": "2022-05-12T00:27:58.162Z", "updated_by": { - "email": "jsmith@email.com", - "full_name": "Joe Smith", - "username": "jsmith" + "email": null, + "full_name": null, + "username": "elastic" }, "connector": { "id": "none", diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml index 7ecb306cf0735..0cc8521456670 100644 --- a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml @@ -15,12 +15,8 @@ value: "parent": null } }, - "description": "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!", - "tags": [ - "phishing", - "social engineering", - "bubblegum" - ], + "description": "A case description.", + "tags": [ "tag-1" ], "settings": { "syncAlerts": true } diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml index c7b02cd47deaa..7413547e6ff60 100644 --- a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml @@ -7,33 +7,29 @@ value: "comments": [], "totalComment": 0, "totalAlerts": 0, - "title": "This case will self-destruct in 5 seconds", - "tags": [ - "phishing", - "social engineering", - "bubblegum" - ], + "title": "Case title 1", + "tags": [ "tag-1" ], "settings": { "syncAlerts": true }, - "owner": "securitySolution", - "description": "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!", + "owner": "cases", + "description": "A case description.", "duration": null, "severity": "low", "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", "created_by": { - "email": "ahunley@imf.usa.gov", - "full_name": "Alan Hunley", - "username": "ahunley" + "email": null, + "full_name": null, + "username": "elastic" }, "status": "open", "updated_at": "2022-05-13T09:48:33.043Z", "updated_by": { - "email": "classified@hms.oo.gov.uk", - "full_name": "Classified", - "username": "M" + "email": null, + "full_name": null, + "username": "elastic" }, "connector": { "id": "131d4448-abe0-4789-939d-8ef60680b498", @@ -48,9 +44,9 @@ value: "external_service": { "external_title": "IS-4", "pushed_by": { - "full_name": "Classified", - "email": "classified@hms.oo.gov.uk", - "username": "M" + "full_name": null, + "email": null, + "username": "elastic" }, "external_url": "https://hms.atlassian.net/browse/IS-4", "pushed_at": "2022-05-13T09:20:40.672Z", 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 0000000000000..8677b327b91be --- /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 0000000000000..65cce12afaa92 --- /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 3f5e5ae73ad19..3c5e511742bf2 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 0000000000000..8041c4e340125 --- /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 cb1df95b13d98..dcc3715377255 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,25 +26,29 @@ 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" + example: "A case description." 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. + 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. If the case + was closed after less than half a second, the duration is rounded down to + zero. example: 120 external_service: type: object @@ -75,7 +79,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: @@ -92,10 +96,10 @@ tags: type: array items: type: string - example: ["phishing","social engineering","bubblegum"] + example: ["tag-1"] title: type: string - example: "This case will self-destruct in 5 seconds" + example: Case title 1 totalAlerts: type: integer example: 0 @@ -120,4 +124,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 f09063d0db18f..6879f820d6f5c 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 c2bc2ab7c887a..fbaa7ee66b568 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 24c1ec5880828..2c31b93e2c2db 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 f39324a36e702..9036fd5a3833a 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 6e1ef2bd1aa1a..c43e207641d96 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 4956056e56b54..772042179d439 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 @@ -80,7 +85,7 @@ delete: required: true schema: type: string - example: 'd4e7abb0-b462-11ec-9a8d-698504725a43' + example: d4e7abb0-b462-11ec-9a8d-698504725a43 responses: '204': description: Indicates a successful call. @@ -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 cc6363d783900..cd846276eee81 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 @@ -112,14 +112,14 @@ get: - type: array items: type: string - example: phishing + example: tag-1 - name: to in: query description: Returns only cases that were created before a specific date. The date must be specified as a KQL data range or date match expression. 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 0000000000000..d79a3c7264b0e --- /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 0000000000000..6a685e903c89d --- /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 0000000000000..1a8de6950fc86 --- /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 368598ec5fbbf..00b2e6661183e 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 @@ -82,7 +87,7 @@ delete: required: true schema: type: string - example: 'd4e7abb0-b462-11ec-9a8d-698504725a43' + example: d4e7abb0-b462-11ec-9a8d-698504725a43 responses: '204': description: Indicates a successful call. @@ -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@_find.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@_find.yaml index 58c41c6827c51..589b9f970ead6 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@_find.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@_find.yaml @@ -112,7 +112,7 @@ get: - type: array items: type: string - example: phishing + example: tag-1 - name: to in: query description: > 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 0000000000000..e0d1bd3201ff9 --- /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 0000000000000..886ed02d84b9c --- /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 0000000000000..550383641934a --- /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/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 523593e68c2f0..bd8e5f325175a 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -72,6 +72,7 @@ export interface AppMockRenderer { render: UiRender; coreStart: StartServices; queryClient: QueryClient; + AppWrapper: React.FC<{ children: React.ReactElement }>; } export const testQueryClient = new QueryClient({ defaultOptions: { @@ -120,6 +121,7 @@ export const createAppMockRenderer = ({ coreStart: services, queryClient, render, + AppWrapper, }; }; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 32ae5ed839b96..ed219e4b9dee0 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -24,8 +24,6 @@ import { useDeleteCases } from '../../containers/use_delete_cases'; import { useGetCases } from '../../containers/use_get_cases'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { useUpdateCases } from '../../containers/use_bulk_update_case'; -import { useGetActionLicense } from '../../containers/use_get_action_license'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { useKibana } from '../../common/lib/kibana'; import { AllCasesList } from './all_cases_list'; import { CasesColumns, GetCasesColumn, useCasesColumns } from './columns'; @@ -34,9 +32,10 @@ import { registerConnectorsToMockActionRegistry } from '../../common/mock/regist import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { waitForComponentToUpdate } from '../../common/test_utils'; import { useCreateAttachments } from '../../containers/use_create_attachments'; -import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../containers/use_create_attachments'); jest.mock('../../containers/use_bulk_update_case'); @@ -59,11 +58,10 @@ const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useGetCasesMetricsMock = useGetCasesMetrics as jest.Mock; const useUpdateCasesMock = useUpdateCases as jest.Mock; -const useGetActionLicenseMock = useGetActionLicense as jest.Mock; const useGetTagsMock = useGetTags as jest.Mock; const useGetReportersMock = useGetReporters as jest.Mock; const useKibanaMock = useKibana as jest.MockedFunction; -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const useCreateAttachmentsMock = useCreateAttachments as jest.Mock; const mockTriggersActionsUiService = triggersActionsUiMock.createStart(); @@ -135,12 +133,6 @@ describe('AllCasesListGeneric', () => { updateBulkStatus, }; - const defaultActionLicense = { - actionLicense: null, - isLoading: false, - isError: false, - }; - const defaultColumnArgs = { caseDetailsNavigation: { href: jest.fn(), @@ -167,8 +159,7 @@ describe('AllCasesListGeneric', () => { useDeleteCasesMock.mockReturnValue(defaultDeleteCases); useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); useGetCasesMetricsMock.mockReturnValue(defaultCasesMetrics); - useGetActionLicenseMock.mockReturnValue(defaultActionLicense); - useGetTagsMock.mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags: jest.fn() }); + useGetTagsMock.mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); useGetReportersMock.mockReturnValue({ reporters: ['casetester'], respReporters: [{ username: 'casetester' }], @@ -176,8 +167,7 @@ describe('AllCasesListGeneric', () => { isError: false, fetchReporters: jest.fn(), }); - useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false })); - useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false })); + useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); mockKibana(); moment.tz.setDefault('UTC'); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 72424785f4069..4417b10754f5f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -25,9 +25,9 @@ import { CasesTableFilters } from './table_filters'; import { EuiBasicTableOnChange } from './types'; import { CasesTable } from './table'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { useCasesContext } from '../cases_context/use_cases_context'; import { CasesMetrics } from './cases_metrics'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; const ProgressLoader = styled(EuiProgress)` ${({ $isShow }: { $isShow: boolean }) => @@ -55,10 +55,11 @@ export interface AllCasesListProps { export const AllCasesList = React.memo( ({ hiddenStatuses = [], isSelectorView = false, onRowClick, doRefresh }) => { const { owner, userCanCrud } = useCasesContext(); - const hasOwner = !!owner.length; const availableSolutions = useAvailableCasesOwners(); const [refresh, setRefresh] = useState(0); + const hasOwner = !!owner.length; + const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses)); const initialFilterOptions = { ...(!isEmpty(hiddenStatuses) && firstAvailableStatus && { status: firstAvailableStatus }), @@ -78,7 +79,7 @@ export const AllCasesList = React.memo( setSelectedCases, } = useGetCases({ initialFilterOptions }); - const { connectors } = useConnectors(); + const { data: connectors = [] } = useGetConnectors(); const sorting = useMemo( () => ({ diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 474e84598de06..bbf575d669306 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -11,24 +11,28 @@ import { waitFor } from '@testing-library/react'; import { AllCases } from '.'; import { TestProviders } from '../../common/mock'; -import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { useGetActionLicense } from '../../containers/use_get_action_license'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { CaseStatuses } from '../../../common/api'; import { casesStatus, connectorsMock, useGetCasesMockState } from '../../containers/mock'; import { useGetCases } from '../../containers/use_get_cases'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../containers/use_get_reporters'); jest.mock('../../containers/use_get_tags'); -jest.mock('../../containers/use_get_action_license'); +jest.mock('../../containers/use_get_action_license', () => { + return { + useGetActionLicense: jest.fn(), + }; +}); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/api'); jest.mock('../../containers/use_get_cases'); jest.mock('../../containers/use_get_cases_status'); -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useGetActionLicenseMock = useGetActionLicense as jest.Mock; @@ -58,13 +62,13 @@ describe('AllCases', () => { }; const defaultActionLicense = { - actionLicense: null, + data: null, isLoading: false, isError: false, }; beforeAll(() => { - (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags: jest.fn() }); + (useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); (useGetReporters as jest.Mock).mockReturnValue({ reporters: ['casetester'], respReporters: [{ username: 'casetester' }], @@ -72,11 +76,7 @@ describe('AllCases', () => { isError: false, fetchReporters: jest.fn(), }); - (useGetActionLicense as jest.Mock).mockReturnValue({ - actionLicense: null, - isLoading: false, - }); - useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false })); + useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); useGetActionLicenseMock.mockReturnValue(defaultActionLicense); useGetCasesMock.mockReturnValue(defaultGetCases); @@ -150,7 +150,7 @@ describe('AllCases', () => { it('should not allow the user to enter configuration page with basic license', async () => { useGetActionLicenseMock.mockReturnValue({ ...defaultActionLicense, - actionLicense: { + data: { id: '.jira', name: 'Jira', minimumLicenseRequired: 'gold', @@ -176,7 +176,7 @@ describe('AllCases', () => { it('should allow the user to enter configuration page with gold license and above', async () => { useGetActionLicenseMock.mockReturnValue({ ...defaultActionLicense, - actionLicense: { + data: { id: '.jira', name: 'Jira', minimumLicenseRequired: 'gold', diff --git a/x-pack/plugins/cases/public/components/all_cases/index.tsx b/x-pack/plugins/cases/public/components/all_cases/index.tsx index c2811df9a684d..465806135a096 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.tsx @@ -18,7 +18,7 @@ export const AllCases: React.FC = () => { const { userCanCrud } = useCasesContext(); useCasesBreadcrumbs(CasesDeepLinkId.cases); - const { actionLicense } = useGetActionLicense(); + const { data: actionLicense = null } = useGetActionLicense(); const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); return ( diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx index 581ecef47ad88..173d4ec76b230 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx @@ -15,9 +15,11 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; import styled from 'styled-components'; +import { QueryClientProvider } from 'react-query'; import { Case, CaseStatusWithAllStatus } from '../../../../common/ui/types'; import * as i18n from '../../../common/translations'; import { AllCasesList } from '../all_cases_list'; +import { casesQueryClient } from '../../cases_context/query_client'; export interface AllCasesSelectorModalProps { hiddenStatuses?: CaseStatusWithAllStatus[]; @@ -53,23 +55,25 @@ export const AllCasesSelectorModal = React.memo( ); return isModalOpen ? ( - - - {i18n.SELECT_CASE_TITLE} - - - - - - - {i18n.CANCEL} - - - + + + + {i18n.SELECT_CASE_TITLE} + + + + + + + {i18n.CANCEL} + + + + ) : null; } ); diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index ff1c00b56d031..63c5b55099cf3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -11,18 +11,18 @@ import { mount } from 'enzyme'; import { CaseStatuses } from '../../../common/api'; import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; -import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; import { CasesTableFilters } from './table_filters'; import userEvent from '@testing-library/user-event'; +import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../containers/use_get_reporters'); jest.mock('../../containers/use_get_tags'); const onFilterChanged = jest.fn(); const fetchReporters = jest.fn(); -const fetchTags = jest.fn(); +const refetch = jest.fn(); const setFilterRefetch = jest.fn(); const props = { @@ -40,7 +40,7 @@ describe('CasesTableFilters ', () => { beforeEach(() => { appMockRender = createAppMockRenderer(); jest.clearAllMocks(); - (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags }); + (useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch }); (useGetReporters as jest.Mock).mockReturnValue({ reporters: ['casetester'], respReporters: [{ username: 'casetester' }], diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index 0a34e756e37a6..b022b1d956947 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -18,12 +18,13 @@ import { } from '../../../common/ui/types'; import { CaseStatuses } from '../../../common/api'; import { FilterOptions } from '../../containers/types'; -import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { FilterPopover } from '../filter_popover'; import { StatusFilter } from './status_filter'; import * as i18n from './translations'; import { SeverityFilter } from './severity_filter'; +import { useGetTags } from '../../containers/use_get_tags'; +import { CASE_LIST_CACHE_KEY } from '../../containers/constants'; interface CasesTableFiltersProps { countClosedCases: number | null; @@ -85,7 +86,7 @@ const CasesTableFiltersComponent = ({ const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); const [selectedOwner, setSelectedOwner] = useState(initial.owner); - const { tags, fetchTags } = useGetTags(); + const { data: tags = [], refetch: fetchTags } = useGetTags(CASE_LIST_CACHE_KEY); const { reporters, respReporters, fetchReporters } = useGetReporters(); const refetch = useCallback(() => { diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index e56ac8b0da655..194f31bf4607b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -111,6 +111,5 @@ export const ATTC_STAT = i18n.translate('xpack.cases.casesStats.mttr', { }); export const ATTC_DESCRIPTION = i18n.translate('xpack.cases.casesStats.mttrDescription', { - defaultMessage: - 'Average time to close is the average duration of cases from creation to closure.', + defaultMessage: 'The average duration (from creation to closure) for your current cases', }); diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index 70738bc44feff..75e44c307578c 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { act, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/dom'; +import { act } from '@testing-library/react-hooks'; import userEvent from '@testing-library/user-event'; import { mount } from 'enzyme'; import React from 'react'; @@ -13,25 +14,31 @@ import { ConnectorTypes } from '../../../common/api'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import '../../common/mock/match_media'; import { useCaseViewNavigation, useUrlParams } from '../../common/navigation/hooks'; -import { useConnectors } from '../../containers/configure/use_connectors'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; import { + alertComment, + basicCase, basicCaseClosed, basicCaseMetrics, caseUserActions, connectorsMock, getAlertUserAction, } from '../../containers/mock'; +import { Case } from '../../containers/types'; +import { useGetCase, UseGetCase } from '../../containers/use_get_case'; import { useGetCaseMetrics } from '../../containers/use_get_case_metrics'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; +import { useGetTags } from '../../containers/use_get_tags'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { useUpdateCase } from '../../containers/use_update_case'; import { CaseViewPage } from './case_view_page'; -import { caseData, caseViewProps } from './index.test'; -import { CaseViewPageProps, CASE_VIEW_PAGE_TABS } from './types'; +import { CaseViewPageProps, CaseViewProps, CASE_VIEW_PAGE_TABS } from './types'; +jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_update_case'); jest.mock('../../containers/use_get_case_metrics'); jest.mock('../../containers/use_get_case_user_actions'); +jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_get_case'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/use_post_push_to_service'); @@ -39,13 +46,94 @@ jest.mock('../user_actions/timestamp'); jest.mock('../../common/navigation/hooks'); jest.mock('../../common/hooks'); +const useFetchCaseMock = useGetCase as jest.Mock; const useUrlParamsMock = useUrlParams as jest.Mock; const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock; const useUpdateCaseMock = useUpdateCase as jest.Mock; const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; const useGetCaseMetricsMock = useGetCaseMetrics as jest.Mock; +const useGetTagsMock = useGetTags as jest.Mock; + +const alertsHit = [ + { + _id: 'alert-id-1', + _index: 'alert-index-1', + _source: { + signal: { + rule: { + id: 'rule-id-1', + name: 'Awesome rule', + }, + }, + }, + }, + { + _id: 'alert-id-2', + _index: 'alert-index-2', + _source: { + signal: { + rule: { + id: 'rule-id-2', + name: 'Awesome rule 2', + }, + }, + }, + }, +]; + +export const caseViewProps: CaseViewProps = { + onComponentInitialized: jest.fn(), + actionsNavigation: { + href: jest.fn(), + onClick: jest.fn(), + }, + ruleDetailsNavigation: { + href: jest.fn(), + onClick: jest.fn(), + }, + showAlertDetails: jest.fn(), + useFetchAlertData: () => [ + false, + { + 'alert-id-1': alertsHit[0], + 'alert-id-2': alertsHit[1], + }, + ], +}; + +export const caseData: Case = { + ...basicCase, + comments: [...basicCase.comments, alertComment], + connector: { + id: 'resilient-2', + name: 'Resilient', + type: ConnectorTypes.resilient, + fields: null, + }, +}; +const defaultGetCase = { + isLoading: false, + isError: false, + data: { + case: caseData, + outcome: 'exactMatch', + }, + refetch: jest.fn(), +}; + +const mockGetCase = (props: Partial = {}) => { + const data = { + ...defaultGetCase.data, + ...props.data, + }; + useFetchCaseMock.mockReturnValue({ + ...defaultGetCase, + ...props, + data, + }); +}; export const caseProps: CaseViewPageProps = { ...caseViewProps, @@ -96,12 +184,14 @@ describe('CaseViewPage', () => { }; beforeEach(() => { + mockGetCase(); jest.clearAllMocks(); useUpdateCaseMock.mockReturnValue(defaultUpdateCaseState); useGetCaseMetricsMock.mockReturnValue(defaultGetCaseMetrics); useGetCaseUserActionsMock.mockReturnValue(defaultUseGetCaseUserActions); usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService }); - useConnectorsMock.mockReturnValue({ connectors: connectorsMock, loading: false }); + useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); + useGetTagsMock.mockReturnValue({ data: [], isLoading: false }); }); it('should render CaseViewPage', async () => { @@ -326,14 +416,6 @@ describe('CaseViewPage', () => { }); it('should disable the push button when connector is invalid', async () => { - useGetCaseUserActionsMock.mockImplementation(() => ({ - ...defaultUseGetCaseUserActions, - data: { - ...defaultUseGetCaseUserActions.data, - hasDataToPush: true, - }, - })); - const wrapper = mount( { }); it('should show the correct connector name on the push button', async () => { - useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false })); + useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); useGetCaseUserActionsMock.mockImplementation(() => ({ ...defaultUseGetCaseUserActions, data: { @@ -559,7 +641,7 @@ describe('CaseViewPage', () => { describe('Callouts', () => { it('it shows the danger callout when a connector has been deleted', async () => { - useConnectorsMock.mockImplementation(() => ({ connectors: [], loading: false })); + useGetConnectorsMock.mockImplementation(() => ({ data: [], isLoading: false })); const wrapper = mount( @@ -573,7 +655,7 @@ describe('CaseViewPage', () => { }); it('it does NOT shows the danger callout when connectors are loading', async () => { - useConnectorsMock.mockImplementation(() => ({ connectors: [], loading: true })); + useGetConnectorsMock.mockImplementation(() => ({ data: [], isLoading: true })); const wrapper = mount( diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx index f9fb435229ae6..f934cc88dc404 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx @@ -19,9 +19,8 @@ import { ConnectorTypes } from '../../../../common/api/connectors'; import { Case } from '../../../../common'; import { CaseViewProps } from '../types'; import { useGetCaseUserActions } from '../../../containers/use_get_case_user_actions'; -import { useConnectors } from '../../../containers/configure/use_connectors'; import { usePostPushToService } from '../../../containers/use_post_push_to_service'; -import { useGetActionLicense } from '../../../containers/use_get_action_license'; +import { useGetConnectors } from '../../../containers/configure/use_connectors'; import { useGetTags } from '../../../containers/use_get_tags'; jest.mock('../../../containers/use_get_case_user_actions'); @@ -32,11 +31,7 @@ jest.mock('../../../common/navigation/hooks'); jest.mock('../../../containers/use_get_action_license'); jest.mock('../../../containers/use_get_tags'); -(useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags: jest.fn() }); -(useGetActionLicense as jest.Mock).mockReturnValue({ - actionLicense: null, - isLoading: false, -}); +(useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); const caseData: Case = { ...basicCase, @@ -90,13 +85,13 @@ export const caseProps = { }; const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; describe('Case View Page activity tab', () => { beforeAll(() => { useGetCaseUserActionsMock.mockReturnValue(defaultUseGetCaseUserActions); - useConnectorsMock.mockReturnValue({ connectors: connectorsMock, loading: false }); + useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService }); }); let appMockRender: AppMockRenderer; diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index dccde3988622f..57b994b733620 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -7,8 +7,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; +import { useGetConnectors } from '../../../containers/configure/use_connectors'; import { CaseSeverity } from '../../../../common/api'; -import { useConnectors } from '../../../containers/configure/use_connectors'; import { useCaseViewNavigation } from '../../../common/navigation'; import { UseFetchAlertData } from '../../../../common/ui/types'; import { Case, CaseStatuses } from '../../../../common'; @@ -88,7 +88,7 @@ export const CaseViewActivity = ({ [onUpdateField] ); - const { loading: isLoadingConnectors, connectors } = useConnectors(); + const { isLoading: isLoadingConnectors, data: connectors = [] } = useGetConnectors(); const [connectorName, isValidConnector] = useMemo(() => { const connector = connectors.find((c) => c.id === caseData.connector.id); diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index b9784c36d1ee6..c1ec02e91c7ab 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -5,12 +5,16 @@ * 2.0. */ +/* x-pack/plugins/cases/public/components/case_view/index.test.tsx + * 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 { mount } from 'enzyme'; -import { act, waitFor } from '@testing-library/react'; import '../../common/mock/match_media'; -import { CaseView } from '.'; import { CaseViewProps } from './types'; import { basicCase, @@ -20,22 +24,29 @@ import { basicCaseMetrics, connectorsMock, } from '../../containers/mock'; -import { TestProviders } from '../../common/mock'; import { SpacesApi } from '@kbn/spaces-plugin/public'; import { useUpdateCase } from '../../containers/use_update_case'; import { UseGetCase, useGetCase } from '../../containers/use_get_case'; import { useGetCaseMetrics } from '../../containers/use_get_case_metrics'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { ConnectorTypes } from '../../../common/api'; import { Case } from '../../../common/ui'; import { useKibana } from '../../common/lib/kibana'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { TestProviders } from '../../common/mock'; +import CaseView from '.'; +import { mount } from 'enzyme'; +import { waitFor } from '@testing-library/dom'; +import { useGetTags } from '../../containers/use_get_tags'; import { QueryClient, QueryClientProvider } from 'react-query'; +import { act } from '@testing-library/react-hooks'; import { CASE_VIEW_CACHE_KEY } from '../../containers/constants'; +jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_update_case'); +jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_get_case_user_actions'); jest.mock('../../containers/use_get_case'); jest.mock('../../containers/use_get_case_metrics'); @@ -50,9 +61,10 @@ const useFetchCaseMock = useGetCase as jest.Mock; const useGetCaseMetricsMock = useGetCaseMetrics as jest.Mock; const useUpdateCaseMock = useUpdateCase as jest.Mock; const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; const useKibanaMock = useKibana as jest.MockedFunction; +const useGetTagsMock = useGetTags as jest.Mock; const spacesUiApiMock = { redirectLegacyUrl: jest.fn().mockResolvedValue(undefined), @@ -183,7 +195,8 @@ describe('CaseView', () => { useUpdateCaseMock.mockReturnValue(defaultUpdateCaseState); useGetCaseUserActionsMock.mockReturnValue(defaultUseGetCaseUserActions); usePostPushToServiceMock.mockReturnValue({ isLoading: false, pushCaseToExternalService }); - useConnectorsMock.mockReturnValue({ connectors: connectorsMock, loading: false }); + useGetConnectorsMock.mockReturnValue({ data: connectorsMock, isLoading: false }); + useGetTagsMock.mockReturnValue({ data: [], isLoading: false }); useKibanaMock().services.spaces = { ui: spacesUiApiMock } as unknown as SpacesApi; }); diff --git a/x-pack/plugins/cases/public/components/case_view/use_on_refresh_case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/use_on_refresh_case_view_page.tsx index 9d2e606192c85..a4c36deabef80 100644 --- a/x-pack/plugins/cases/public/components/case_view/use_on_refresh_case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/use_on_refresh_case_view_page.tsx @@ -7,7 +7,7 @@ import { useCallback } from 'react'; import { useQueryClient } from 'react-query'; -import { CASE_VIEW_CACHE_KEY } from '../../containers/constants'; +import { CASE_TAGS_CACHE_KEY, CASE_VIEW_CACHE_KEY } from '../../containers/constants'; /** * Using react-query queryClient to invalidate all the @@ -21,5 +21,6 @@ export const useRefreshCaseViewPage = () => { const queryClient = useQueryClient(); return useCallback(() => { queryClient.invalidateQueries(CASE_VIEW_CACHE_KEY); + queryClient.invalidateQueries(CASE_TAGS_CACHE_KEY); }, [queryClient]); }; diff --git a/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx index 46dbbdbe9a196..cc76bdfcd2f7d 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx @@ -7,9 +7,7 @@ import { ActionTypeConnector, ConnectorTypes } from '../../../../common/api'; import { ActionConnector } from '../../../containers/configure/types'; -import { UseConnectorsResponse } from '../../../containers/configure/use_connectors'; import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; -import { UseActionTypesResponse } from '../../../containers/configure/use_action_types'; import { connectorsMock, actionTypesMock } from '../../../common/mock/connectors'; export { mappings } from '../../../containers/configure/mock'; @@ -50,14 +48,14 @@ export const useCaseConfigureResponse: ReturnUseCaseConfigure = { id: '', }; -export const useConnectorsResponse: UseConnectorsResponse = { - loading: false, - connectors, - refetchConnectors: jest.fn(), +export const useConnectorsResponse = { + isLoading: false, + data: connectors, + refetch: jest.fn(), }; -export const useActionTypesResponse: UseActionTypesResponse = { - loading: false, - actionTypes: actionTypesMock, - refetchActionTypes: jest.fn(), +export const useActionTypesResponse = { + isLoading: false, + data: actionTypesMock, + refetch: jest.fn(), }; diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 2ee1c328f4699..888ac6576ec23 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -15,9 +15,7 @@ import { Connectors } from './connectors'; import { ClosureOptions } from './closure_options'; import { useKibana } from '../../common/lib/kibana'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; -import { useActionTypes } from '../../containers/configure/use_action_types'; import { connectors, @@ -28,6 +26,8 @@ import { } from './__mock__'; import { ConnectorTypes } from '../../../common/api'; import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; +import { useGetActionTypes } from '../../containers/configure/use_action_types'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; jest.mock('../../common/lib/kibana'); jest.mock('../../containers/configure/use_connectors'); @@ -35,10 +35,10 @@ jest.mock('../../containers/configure/use_configure'); jest.mock('../../containers/configure/use_action_types'); const useKibanaMock = useKibana as jest.Mocked; -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetUrlSearchMock = jest.fn(); -const useActionTypesMock = useActionTypes as jest.Mock; +const useGetActionTypesMock = useGetActionTypes as jest.Mock; const getAddConnectorFlyoutMock = jest.fn(); const getEditConnectorFlyoutMock = jest.fn(); @@ -57,14 +57,14 @@ describe('ConfigureCases', () => { }); beforeEach(() => { - useActionTypesMock.mockImplementation(() => useActionTypesResponse); + useGetActionTypesMock.mockImplementation(() => useActionTypesResponse); }); describe('rendering', () => { let wrapper: ReactWrapper; beforeEach(() => { useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); - useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useGetConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, data: [] })); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { @@ -118,7 +118,7 @@ describe('ConfigureCases', () => { closureType: 'close-by-user', }, })); - useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useGetConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, data: [] })); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders, @@ -164,7 +164,7 @@ describe('ConfigureCases', () => { closureType: 'close-by-user', }, })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { @@ -246,9 +246,9 @@ describe('ConfigureCases', () => { }, })); - useConnectorsMock.mockImplementation(() => ({ + useGetConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, - loading: true, + isLoading: true, })); useGetUrlSearchMock.mockImplementation(() => searchURL); @@ -280,12 +280,15 @@ describe('ConfigureCases', () => { }); test('it shows isLoading when loading action types', () => { - useConnectorsMock.mockImplementation(() => ({ + useGetConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, - loading: false, + isLoading: false, })); - useActionTypesMock.mockImplementation(() => ({ ...useActionTypesResponse, loading: true })); + useGetActionTypesMock.mockImplementation(() => ({ + ...useActionTypesResponse, + isLoading: true, + })); wrapper = mount(, { wrappingComponent: TestProviders, @@ -309,7 +312,7 @@ describe('ConfigureCases', () => { persistLoading: true, })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { wrappingComponent: TestProviders, @@ -350,7 +353,7 @@ describe('ConfigureCases', () => { ...useCaseConfigureResponse, loading: true, })); - useConnectorsMock.mockImplementation(() => ({ + useGetConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, })); useGetUrlSearchMock.mockImplementation(() => searchURL); @@ -395,7 +398,7 @@ describe('ConfigureCases', () => { }, persistCaseConfigure, })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { @@ -486,7 +489,7 @@ describe('ConfigureCases', () => { }, persistCaseConfigure, })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); wrapper = mount(, { @@ -533,7 +536,7 @@ describe('ConfigureCases', () => { closureType: 'close-by-user', }, })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 61172b9998667..16773306d6bef 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -15,8 +15,7 @@ import { EuiCallOut, EuiLink } from '@elastic/eui'; import { ActionConnectorTableItem } from '@kbn/triggers-actions-ui-plugin/public/types'; import { SUPPORTED_CONNECTORS } from '../../../common/constants'; import { useKibana } from '../../common/lib/kibana'; -import { useConnectors } from '../../containers/configure/use_connectors'; -import { useActionTypes } from '../../containers/configure/use_action_types'; +import { useGetActionTypes } from '../../containers/configure/use_action_types'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { ClosureType } from '../../containers/configure/types'; @@ -31,6 +30,7 @@ import { HeaderPage } from '../header_page'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesBreadcrumbs } from '../use_breadcrumbs'; import { CasesDeepLinkId } from '../../common/navigation'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; const FormWrapper = styled.div` ${({ theme }) => css` @@ -74,8 +74,17 @@ export const ConfigureCases: React.FC = React.memo(() => { setClosureType, } = useCaseConfigure(); - const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); - const { loading: isLoadingActionTypes, actionTypes, refetchActionTypes } = useActionTypes(); + const { + isLoading: isLoadingConnectors, + data: connectors = [], + refetch: refetchConnectors, + } = useGetConnectors(); + const { + isLoading: isLoadingActionTypes, + data: actionTypes = [], + refetch: refetchActionTypes, + } = useGetActionTypes(); + const supportedActionTypes = useMemo( () => actionTypes.filter((actionType) => SUPPORTED_CONNECTORS.includes(actionType.id)), [actionTypes] diff --git a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx index 6a2ac8e15dedb..148cfa119bebd 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx @@ -9,11 +9,13 @@ import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; +import { QueryClientProvider } from 'react-query'; import * as i18n from '../translations'; import { Case } from '../../../../common/ui/types'; import { CreateCaseForm } from '../form'; import { UseCreateAttachments } from '../../../containers/use_create_attachments'; import { CaseAttachments } from '../../../types'; +import { casesQueryClient } from '../../cases_context/query_client'; export interface CreateCaseFlyoutProps { afterCaseCreated?: ( @@ -73,7 +75,7 @@ export const CreateCaseFlyout = React.memo( const handleCancel = onClose || function () {}; const handleOnSuccess = onSuccess || async function () {}; return ( - <> + ( - + ); } ); diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index c073e9190b495..169e4029b982b 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -11,14 +11,14 @@ import { act, render } from '@testing-library/react'; import { NONE_CONNECTOR_ID } from '../../../common/api'; import { useForm, Form, FormHook } from '../../common/shared_imports'; -import { useGetTags } from '../../containers/use_get_tags'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/mock'; import { schema, FormProps } from './schema'; import { CreateCaseForm, CreateCaseFormProps } from './form'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { TestProviders } from '../../common/mock'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); @@ -29,7 +29,7 @@ jest.mock('../app/use_available_owners', () => ({ })); const useGetTagsMock = useGetTags as jest.Mock; -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const initialCaseValue: FormProps = { @@ -69,8 +69,8 @@ describe('CreateCaseForm', () => { beforeEach(() => { jest.clearAllMocks(); - useGetTagsMock.mockReturnValue({ tags: ['test'] }); - useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + useGetTagsMock.mockReturnValue({ data: ['test'] }); + useGetConnectorsMock.mockReturnValue({ isLoading: false, data: connectorsMock }); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); }); diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index bfa4f391458da..bdc6f68ddf077 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -15,8 +15,6 @@ import { useKibana } from '../../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; import { useCreateAttachments } from '../../containers/use_create_attachments'; -import { useGetTags } from '../../containers/use_get_tags'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; import { useGetSeverity } from '../connectors/resilient/use_get_severity'; @@ -42,6 +40,8 @@ import { Choice } from '../connectors/servicenow/types'; import userEvent from '@testing-library/user-event'; import { connectorsMock } from '../../common/mock/connectors'; import { CaseAttachments } from '../../types'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetTags } from '../../containers/use_get_tags'; const sampleId = 'case-id'; @@ -60,7 +60,7 @@ jest.mock('../connectors/jira/use_get_issues'); jest.mock('../connectors/servicenow/use_get_choices'); jest.mock('../../common/lib/kibana'); -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const usePostCaseMock = usePostCase as jest.Mock; const useCreateAttachmentsMock = useCreateAttachments as jest.Mock; @@ -130,7 +130,7 @@ const fillFormReactTestingLib = async (renderResult: RenderResult) => { }; describe('Create case', () => { - const fetchTags = jest.fn(); + const refetch = jest.fn(); const onFormSubmitSuccess = jest.fn(); const afterCaseCreated = jest.fn(); const createAttachments = jest.fn(); @@ -145,7 +145,7 @@ describe('Create case', () => { usePostCaseMock.mockImplementation(() => defaultPostCase); useCreateAttachmentsMock.mockImplementation(() => ({ createAttachments })); usePostPushToServiceMock.mockImplementation(() => defaultPostPushToService); - useConnectorsMock.mockReturnValue(sampleConnectorData); + useGetConnectorsMock.mockReturnValue(sampleConnectorData); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); useGetSeverityMock.mockReturnValue(useGetSeverityResponse); @@ -159,8 +159,8 @@ describe('Create case', () => { ); (useGetTags as jest.Mock).mockImplementation(() => ({ - tags: sampleTags, - fetchTags, + data: sampleTags, + refetch, })); useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ actionTypeTitle: '.servicenow', @@ -190,9 +190,9 @@ describe('Create case', () => { }); it('should post case on submit click', async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const renderResult = mockedContext.render( @@ -210,9 +210,9 @@ describe('Create case', () => { }); it('should post a case on submit click with the selected severity', async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const renderResult = mockedContext.render( @@ -268,9 +268,9 @@ describe('Create case', () => { }); it('should toggle sync settings', async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mount( @@ -292,9 +292,9 @@ describe('Create case', () => { }); it('should set sync alerts to false when the sync feature setting is false', async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mount( @@ -338,9 +338,9 @@ describe('Create case', () => { persistLoading: false, })); - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mount( @@ -388,9 +388,9 @@ describe('Create case', () => { persistLoading: false, })); - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mount( @@ -413,9 +413,9 @@ describe('Create case', () => { describe('Step 2 - Connector Fields', () => { it(`should submit and push to Jira connector`, async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mount( @@ -480,9 +480,9 @@ describe('Create case', () => { }); it(`should submit and push to resilient connector`, async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mount( @@ -550,9 +550,9 @@ describe('Create case', () => { }); it(`should submit and push to servicenow itsm connector`, async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mount( @@ -645,9 +645,9 @@ describe('Create case', () => { }); it(`should submit and push to servicenow sir connector`, async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mount( @@ -748,9 +748,9 @@ describe('Create case', () => { }); it(`should call afterCaseCreated`, async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const wrapper = mockedContext.render( @@ -781,9 +781,9 @@ describe('Create case', () => { }); it('should call createAttachments with the attachments after the case is created', async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const attachments = [ { @@ -826,9 +826,9 @@ describe('Create case', () => { }); it('should NOT call createAttachments if the attachments are an empty array', async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const attachments: CaseAttachments = []; @@ -849,9 +849,9 @@ describe('Create case', () => { }); it(`should call callbacks in correct order`, async () => { - useConnectorsMock.mockReturnValue({ + useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, - connectors: connectorsMock, + data: connectorsMock, }); const attachments = [ { diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index a65e9f5960e9d..70b4fb4ec9ab0 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -12,7 +12,6 @@ import { getNoneConnector, normalizeActionConnector } from '../configure_cases/u import { usePostCase } from '../../containers/use_post_case'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { Case } from '../../containers/types'; import { CaseSeverity, NONE_CONNECTOR_ID } from '../../../common/api'; import { @@ -23,6 +22,7 @@ import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesFeatures } from '../cases_context/use_cases_features'; import { getConnectorById } from '../utils'; import { CaseAttachments } from '../../types'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; const initialCaseValue: FormProps = { description: '', @@ -51,7 +51,7 @@ export const FormContext: React.FC = ({ onSuccess, attachments, }) => { - const { connectors, loading: isLoadingConnectors } = useConnectors(); + const { data: connectors = [], isLoading: isLoadingConnectors } = useGetConnectors(); const { owner } = useCasesContext(); const { isSyncAlertsEnabled } = useCasesFeatures(); const { postCase } = usePostCase(); diff --git a/x-pack/plugins/cases/public/components/create/index.test.tsx b/x-pack/plugins/cases/public/components/create/index.test.tsx index 64935321eabac..c455ae0c3628d 100644 --- a/x-pack/plugins/cases/public/components/create/index.test.tsx +++ b/x-pack/plugins/cases/public/components/create/index.test.tsx @@ -11,8 +11,6 @@ import { act } from '@testing-library/react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { TestProviders } from '../../common/mock'; -import { useGetTags } from '../../containers/use_get_tags'; -import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; import { useGetSeverity } from '../connectors/resilient/use_get_severity'; @@ -29,6 +27,8 @@ import { useGetFieldsByIssueTypeResponse, } from './mock'; import { CreateCase } from '.'; +import { useGetConnectors } from '../../containers/configure/use_connectors'; +import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../containers/api'); jest.mock('../../containers/use_get_tags'); @@ -41,7 +41,7 @@ jest.mock('../connectors/jira/use_get_fields_by_issue_type'); jest.mock('../connectors/jira/use_get_single_issue'); jest.mock('../connectors/jira/use_get_issues'); -const useConnectorsMock = useConnectors as jest.Mock; +const useGetConnectorsMock = useGetConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetTagsMock = useGetTags as jest.Mock; const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; @@ -78,15 +78,15 @@ const defaultProps = { describe('CreateCase case', () => { beforeEach(() => { jest.clearAllMocks(); - useConnectorsMock.mockReturnValue(sampleConnectorData); + useGetConnectorsMock.mockReturnValue(sampleConnectorData); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); useGetSeverityMock.mockReturnValue(useGetSeverityResponse); useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); useGetTagsMock.mockImplementation(() => ({ - tags: sampleTags, - fetchTags, + data: sampleTags, + refetch: fetchTags, })); }); diff --git a/x-pack/plugins/cases/public/components/create/mock.ts b/x-pack/plugins/cases/public/components/create/mock.ts index 38d57bf24781e..8f67b3c05d3e4 100644 --- a/x-pack/plugins/cases/public/components/create/mock.ts +++ b/x-pack/plugins/cases/public/components/create/mock.ts @@ -27,7 +27,7 @@ export const sampleData: CasePostRequest = { owner: SECURITY_SOLUTION_OWNER, }; -export const sampleConnectorData = { loading: false, connectors: [] }; +export const sampleConnectorData = { isLoading: false, data: [] }; export const useGetIncidentTypesResponse = { isLoading: false, diff --git a/x-pack/plugins/cases/public/components/create/tags.test.tsx b/x-pack/plugins/cases/public/components/create/tags.test.tsx index fbb261b3d0682..dde659c2da8ba 100644 --- a/x-pack/plugins/cases/public/components/create/tags.test.tsx +++ b/x-pack/plugins/cases/public/components/create/tags.test.tsx @@ -11,10 +11,10 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { waitFor } from '@testing-library/react'; import { useForm, Form, FormHook } from '../../common/shared_imports'; -import { useGetTags } from '../../containers/use_get_tags'; import { Tags } from './tags'; import { schema, FormProps } from './schema'; import { TestProviders } from '../../common/mock'; +import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../common/lib/kibana'); jest.mock('../../containers/use_get_tags'); @@ -43,7 +43,7 @@ describe('Tags', () => { beforeEach(() => { jest.clearAllMocks(); - useGetTagsMock.mockReturnValue({ tags: ['test'] }); + useGetTagsMock.mockReturnValue({ data: ['test'] }); }); it('it renders', async () => { diff --git a/x-pack/plugins/cases/public/components/create/tags.tsx b/x-pack/plugins/cases/public/components/create/tags.tsx index ac0b67529e15a..11dac39bba75b 100644 --- a/x-pack/plugins/cases/public/components/create/tags.tsx +++ b/x-pack/plugins/cases/public/components/create/tags.tsx @@ -17,7 +17,7 @@ interface Props { } const TagsComponent: React.FC = ({ isLoading }) => { - const { tags: tagOptions, isLoading: isLoadingTags } = useGetTags(); + const { data: tagOptions = [], isLoading: isLoadingTags } = useGetTags(); const options = useMemo( () => tagOptions.map((label) => ({ diff --git a/x-pack/plugins/cases/public/components/recent_cases/index.tsx b/x-pack/plugins/cases/public/components/recent_cases/index.tsx index 0b4e65cf68709..4cc070e77b76a 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/index.tsx @@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText, EuiTitle } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { QueryClientProvider } from 'react-query'; import * as i18n from './translations'; import { LinkAnchor } from '../links'; import { RecentCasesFilters } from './filters'; @@ -15,6 +16,7 @@ import { RecentCasesComp } from './recent_cases'; import { FilterMode as RecentCasesFilterMode } from './types'; import { useCurrentUser } from '../../common/lib/kibana'; import { useAllCasesNavigation } from '../../common/navigation'; +import { casesQueryClient } from '../cases_context/query_client'; export interface RecentCasesProps { maxCasesToShow: number; @@ -52,7 +54,7 @@ const RecentCases = React.memo(({ maxCasesToShow }: RecentCasesProps) => { ); return ( - <> + <> @@ -81,7 +83,7 @@ const RecentCases = React.memo(({ maxCasesToShow }: RecentCasesProps) => { - + ); }); diff --git a/x-pack/plugins/cases/public/components/tag_list/index.test.tsx b/x-pack/plugins/cases/public/components/tag_list/index.test.tsx index da1d8c1c63ab1..f795f3ae851ad 100644 --- a/x-pack/plugins/cases/public/components/tag_list/index.test.tsx +++ b/x-pack/plugins/cases/public/components/tag_list/index.test.tsx @@ -48,8 +48,8 @@ describe('TagList ', () => { (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); (useGetTags as jest.Mock).mockImplementation(() => ({ - tags: sampleTags, - fetchTags, + data: sampleTags, + refetch: fetchTags, })); }); 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 44643412faa5f..9c14c2b1aee53 100644 --- a/x-pack/plugins/cases/public/components/tag_list/index.tsx +++ b/x-pack/plugins/cases/public/components/tag_list/index.tsx @@ -70,11 +70,13 @@ export const TagList = React.memo( const { isValid, data: newData } = await submit(); if (isValid && newData.tags) { onSubmit(newData.tags); + form.reset({ defaultValue: newData }); setIsEditTags(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [onSubmit, submit]); - const { tags: tagOptions } = useGetTags(); + const { data: tagOptions = [] } = useGetTags(); const [options, setOptions] = useState( tagOptions.map((label) => ({ label, diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx index a0671f853a4c9..15cfefd57ac57 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx @@ -15,15 +15,21 @@ import { TestProviders } from '../../common/mock'; import { CaseStatuses, ConnectorTypes } from '../../../common/api'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { basicPush, actionLicenses, connectorsMock } from '../../containers/mock'; -import { useGetActionLicense } from '../../containers/use_get_action_license'; import { CLOSED_CASE_PUSH_ERROR_ID } from './callout/types'; import * as i18n from './translations'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; -jest.mock('../../containers/use_get_action_license'); +jest.mock('../../containers/use_get_action_license', () => { + return { + useGetActionLicense: jest.fn(), + }; +}); jest.mock('../../containers/use_post_push_to_service'); jest.mock('../../containers/configure/api'); jest.mock('../../common/navigation/hooks'); +const useFetchActionLicenseMock = useGetActionLicense as jest.Mock; + describe('usePushToService', () => { const caseId = '12345'; const onEditClick = jest.fn(); @@ -70,9 +76,9 @@ describe('usePushToService', () => { beforeEach(() => { jest.clearAllMocks(); (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + useFetchActionLicenseMock.mockImplementation(() => ({ isLoading: false, - actionLicense, + data: actionLicense, })); }); @@ -100,9 +106,9 @@ describe('usePushToService', () => { }); it('Displays message when user does not have premium license', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + useFetchActionLicenseMock.mockImplementation(() => ({ isLoading: false, - actionLicense: { + data: { ...actionLicense, enabledInLicense: false, }, @@ -122,9 +128,9 @@ describe('usePushToService', () => { }); it('Displays message when user does not have case enabled in config', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + useFetchActionLicenseMock.mockImplementation(() => ({ isLoading: false, - actionLicense: { + data: { ...actionLicense, enabledInConfig: false, }, @@ -278,9 +284,9 @@ describe('usePushToService', () => { const noWriteProps = { ...defaultArgs, userCanCrud: false }; it('does not display a message when user does not have a premium license', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + useFetchActionLicenseMock.mockImplementation(() => ({ isLoading: false, - actionLicense: { + data: { ...actionLicense, enabledInLicense: false, }, @@ -298,9 +304,9 @@ describe('usePushToService', () => { }); it('does not display a message when user does not have case enabled in config', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + useFetchActionLicenseMock.mockImplementation(() => ({ isLoading: false, - actionLicense: { + data: { ...actionLicense, enabledInConfig: false, }, diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx index c644279cf0880..b2c4e79a35596 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx @@ -8,7 +8,6 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { useGetActionLicense } from '../../containers/use_get_action_license'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { CaseCallOut } from './callout'; import { @@ -23,6 +22,7 @@ import { CaseConnector, ActionConnector, CaseStatuses } from '../../../common/ap import { CaseServices } from '../../containers/use_get_case_user_actions'; import { ErrorMessage } from './callout/types'; import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; export interface UsePushToService { caseId: string; @@ -54,7 +54,7 @@ export const usePushToService = ({ }: UsePushToService): ReturnUsePushToService => { const { isLoading, pushCaseToExternalService } = usePostPushToService(); - const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); + const { isLoading: loadingLicense, data: actionLicense = null } = useGetActionLicense(); const hasLicenseError = actionLicense != null && !actionLicense.enabledInLicense; const refreshCaseViewPage = useRefreshCaseViewPage(); diff --git a/x-pack/plugins/fleet/common/constants/data_streams.ts b/x-pack/plugins/cases/public/containers/__mocks__/use_get_action_license.tsx similarity index 56% rename from x-pack/plugins/fleet/common/constants/data_streams.ts rename to x-pack/plugins/cases/public/containers/__mocks__/use_get_action_license.tsx index bb880af9b3df8..e0e07300830cd 100644 --- a/x-pack/plugins/fleet/common/constants/data_streams.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/use_get_action_license.tsx @@ -5,10 +5,13 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { actionLicenses } from '../mock'; -export const GetDataStreamsListRequestSchema = { - params: schema.object({ - use_terms_enum: schema.boolean({ defaultValue: false }), - }), +export const useGetActionLicense = () => { + return { + data: actionLicenses[0], + isLoading: false, + isError: false, + isFetching: false, + }; }; diff --git a/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx index 3b19e74d09208..df0804690b1b9 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx @@ -5,99 +5,44 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; -import { useActionTypes, UseActionTypesResponse } from './use_action_types'; +import { renderHook } from '@testing-library/react-hooks'; import * as api from './api'; -import { actionTypesMock } from '../../common/mock/connectors'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import { useGetActionTypes } from './use_action_types'; +import { useToasts } from '../../common/lib/kibana'; jest.mock('./api'); jest.mock('../../common/lib/kibana'); describe('useActionTypes', () => { + let appMockRenderer: AppMockRenderer; beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); + appMockRenderer = createAppMockRenderer(); }); - test('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useActionTypes() - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: true, - actionTypes: [], - refetchActionTypes: result.current.refetchActionTypes, - }); + it('should fetch action types', async () => { + const spy = jest.spyOn(api, 'fetchActionTypes'); + const { waitForNextUpdate } = renderHook(() => useGetActionTypes(), { + wrapper: appMockRenderer.AppWrapper, }); - }); - - test('fetch action types', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useActionTypes() - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: false, - actionTypes: actionTypesMock, - refetchActionTypes: result.current.refetchActionTypes, - }); - }); - }); - - test('refetch actionTypes', async () => { - const spyOnfetchActionTypes = jest.spyOn(api, 'fetchActionTypes'); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useActionTypes() - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.refetchActionTypes(); - expect(spyOnfetchActionTypes).toHaveBeenCalledTimes(2); - }); + await waitForNextUpdate(); + expect(spy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); }); - test('set isLoading to true when refetching actionTypes', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useActionTypes() - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.refetchActionTypes(); - - expect(result.current.loading).toBe(true); - }); - }); - - test('unhappy path', async () => { - const spyOnfetchActionTypes = jest.spyOn(api, 'fetchActionTypes'); - spyOnfetchActionTypes.mockImplementation(() => { + it('should show a toast eror message if failed to fetch', async () => { + const spy = jest.spyOn(api, 'fetchActionTypes'); + spy.mockImplementation(() => { throw new Error('Something went wrong'); }); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useActionTypes() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - actionTypes: [], - refetchActionTypes: result.current.refetchActionTypes, - }); + const addErrorMock = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addError: addErrorMock }); + const { waitForNextUpdate } = renderHook(() => useGetActionTypes(), { + wrapper: appMockRenderer.AppWrapper, }); + await waitForNextUpdate(); + expect(addErrorMock).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx b/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx index eaaadd65d29d1..caca1de7afcec 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx @@ -5,67 +5,28 @@ * 2.0. */ -import { useState, useEffect, useCallback, useRef } from 'react'; - +import { useQuery } from 'react-query'; import * as i18n from '../translations'; import { fetchActionTypes } from './api'; -import { ActionTypeConnector } from './types'; import { useToasts } from '../../common/lib/kibana'; +import { CASE_CONFIGURATION_CACHE_KEY } from '../constants'; +import { ServerError } from '../../types'; -export interface UseActionTypesResponse { - loading: boolean; - actionTypes: ActionTypeConnector[]; - refetchActionTypes: () => void; -} - -export const useActionTypes = (): UseActionTypesResponse => { +export const useGetActionTypes = () => { const toasts = useToasts(); - const [loading, setLoading] = useState(true); - const [actionTypes, setActionTypes] = useState([]); - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - const queryFirstTime = useRef(true); - - const refetchActionTypes = useCallback(async () => { - try { - setLoading(true); - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - - const res = await fetchActionTypes({ signal: abortCtrlRef.current.signal }); - - if (!isCancelledRef.current) { - setLoading(false); - setActionTypes(res); - } - } catch (error) { - if (!isCancelledRef.current) { - setLoading(false); - setActionTypes([]); + return useQuery( + [CASE_CONFIGURATION_CACHE_KEY, 'actionTypes'], + () => { + const abortController = new AbortController(); + return fetchActionTypes({ signal: abortController.signal }); + }, + { + initialData: [], + onError: (error: ServerError) => { toasts.addError(error.body && error.body.message ? new Error(error.body.message) : error, { title: i18n.ERROR_TITLE, }); - } + }, } - }, [toasts]); - - useEffect(() => { - if (queryFirstTime.current) { - refetchActionTypes(); - queryFirstTime.current = false; - } - - return () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); - queryFirstTime.current = true; - }; - }, [refetchActionTypes]); - - return { - loading, - actionTypes, - refetchActionTypes, - }; + ); }; diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx index b1a3bac22d56f..076e1a8408482 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx @@ -6,12 +6,11 @@ */ import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { useConnectors, UseConnectorsResponse } from './use_connectors'; +import { renderHook } from '@testing-library/react-hooks'; import * as api from './api'; -import { connectorsMock } from '../mock'; import { TestProviders } from '../../common/mock'; -import { useApplicationCapabilities } from '../../common/lib/kibana'; +import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; +import { useGetConnectors } from './use_connectors'; const useApplicationCapabilitiesMock = useApplicationCapabilities as jest.Mocked< typeof useApplicationCapabilities @@ -25,140 +24,45 @@ describe('useConnectors', () => { jest.clearAllMocks(); }); - it('init', async () => { - await act(async () => { - const { result, waitFor } = renderHook(() => useConnectors(), { - wrapper: ({ children }) => {children}, - }); - - await waitFor(() => { - expect(result.current).toEqual({ - loading: true, - connectors: [], - refetchConnectors: result.current.refetchConnectors, - }); - }); + it('fetches connectors', async () => { + const spy = jest.spyOn(api, 'fetchConnectors'); + const { waitForNextUpdate } = renderHook(() => useGetConnectors(), { + wrapper: ({ children }) => {children}, }); - }); - - it('fetch connectors', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useConnectors(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - connectors: connectorsMock, - refetchConnectors: result.current.refetchConnectors, - }); - }); - }); + await waitForNextUpdate(); - it('refetch connectors', async () => { - const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useConnectors(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - result.current.refetchConnectors(); - expect(spyOnfetchConnectors).toHaveBeenCalledTimes(2); - }); + expect(spy).toHaveBeenCalledWith({ signal: expect.any(AbortSignal) }); }); - it('set isLoading to true when refetching connectors', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useConnectors(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - result.current.refetchConnectors(); - - expect(result.current.loading).toBe(true); - }); - }); + it('shows a toast error when the API returns error', async () => { + const addError = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addError }); - it('unhappy path', async () => { const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); spyOnfetchConnectors.mockImplementation(() => { throw new Error('Something went wrong'); }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useConnectors(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - connectors: [], - refetchConnectors: result.current.refetchConnectors, - }); + const { waitForNextUpdate } = renderHook(() => useGetConnectors(), { + wrapper: ({ children }) => {children}, }); + await waitForNextUpdate(); + + expect(addError).toHaveBeenCalled(); }); it('does not fetch connectors when the user does not has access to actions', async () => { const spyOnFetchConnectors = jest.spyOn(api, 'fetchConnectors'); useApplicationCapabilitiesMock().actions = { crud: false, read: false }; - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useConnectors(), - { - wrapper: ({ children }) => {children}, - } - ); - - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - connectors: [], - refetchConnectors: result.current.refetchConnectors, - }); + const { result, waitForNextUpdate } = renderHook(() => useGetConnectors(), { + wrapper: ({ children }) => {children}, }); - expect(spyOnFetchConnectors).not.toHaveBeenCalled(); - }); - - it('does not refetch connectors when the user does not has access to actions', async () => { - const spyOnFetchConnectors = jest.spyOn(api, 'fetchConnectors'); - useApplicationCapabilitiesMock().actions = { crud: false, read: false }; - - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useConnectors(), - { - wrapper: ({ children }) => {children}, - } - ); - - await waitForNextUpdate(); - result.current.refetchConnectors(); - - expect(result.current).toEqual({ - loading: false, - connectors: [], - refetchConnectors: result.current.refetchConnectors, - }); - }); + await waitForNextUpdate(); expect(spyOnFetchConnectors).not.toHaveBeenCalled(); + expect(result.current.data).toEqual([]); }); }); diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx index e8176f5f397e8..9f1d3f38655ae 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx @@ -5,95 +5,34 @@ * 2.0. */ -import { useState, useEffect, useCallback, useRef } from 'react'; - +import { useQuery } from 'react-query'; import { fetchConnectors } from './api'; -import { ActionConnector } from './types'; import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; import * as i18n from './translations'; +import { CASE_CONNECTORS_CACHE_KEY } from '../constants'; +import { ServerError } from '../../types'; -interface ConnectorsState { - loading: boolean; - connectors: ActionConnector[]; -} - -export interface UseConnectorsResponse { - loading: boolean; - connectors: ActionConnector[]; - refetchConnectors: () => void; - permissionsError?: string; -} - -/** - * Retrieves the configured case connectors - * - * @param toastPermissionsErrors boolean controlling whether 403 and 401 errors should be displayed in a toast error - */ -export const useConnectors = (): UseConnectorsResponse => { +export function useGetConnectors() { const toasts = useToasts(); const { actions } = useApplicationCapabilities(); - const [state, setState] = useState({ - loading: true, - connectors: [], - }); - - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - const refetchConnectors = useCallback(async () => { - if (!actions.read) { - setState({ - loading: false, - connectors: [], - }); - - return; - } - - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - setState({ - ...state, - loading: true, - }); - const res = await fetchConnectors({ signal: abortCtrlRef.current.signal }); - - if (!isCancelledRef.current) { - setState({ - loading: false, - connectors: res, - }); + return useQuery( + [CASE_CONNECTORS_CACHE_KEY], + async () => { + if (!actions.read) { + return []; } - } catch (error) { - if (!isCancelledRef.current) { + const abortCtrl = new AbortController(); + return fetchConnectors({ signal: abortCtrl.signal }); + }, + { + onError: (error: ServerError) => { if (error.name !== 'AbortError') { toasts.addError( error.body && error.body.message ? new Error(error.body.message) : error, { title: i18n.ERROR_TITLE } ); } - setState({ - loading: false, - connectors: [], - }); - } + }, } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - refetchConnectors(); - return () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { - loading: state.loading, - connectors: state.connectors, - refetchConnectors, - }; -}; + ); +} diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index 8b45ca1e9f607..cfd7d5020e20f 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -11,3 +11,8 @@ export const DEFAULT_TABLE_LIMIT = 5; export const CASE_VIEW_CACHE_KEY = 'case'; export const CASE_VIEW_ACTIONS_CACHE_KEY = 'user-actions'; export const CASE_VIEW_METRICS_CACHE_KEY = 'metrics'; +export const CASE_CONFIGURATION_CACHE_KEY = 'case-configuration'; +export const CASE_LIST_CACHE_KEY = 'case-list'; +export const CASE_CONNECTORS_CACHE_KEY = 'case-connectors'; +export const CASE_LICENSE_CACHE_KEY = 'case-license-action'; +export const CASE_TAGS_CACHE_KEY = 'case-tags'; diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.test.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.test.tsx index ae6a884514161..9ad55fe496a0e 100644 --- a/x-pack/plugins/cases/public/containers/use_get_action_license.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_action_license.test.tsx @@ -5,87 +5,48 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; -import { initialData, useGetActionLicense, ActionLicenseState } from './use_get_action_license'; -import { actionLicenses } from './mock'; +import { renderHook } from '@testing-library/react-hooks'; import * as api from './api'; +import { useGetActionLicense } from './use_get_action_license'; +import { AppMockRenderer, createAppMockRenderer } from '../common/mock'; +import { useToasts } from '../common/lib/kibana'; jest.mock('./api'); jest.mock('../common/lib/kibana'); describe('useGetActionLicense', () => { const abortCtrl = new AbortController(); + let appMockRenderer: AppMockRenderer; beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); - }); - - it('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetActionLicense() - ); - await waitForNextUpdate(); - expect(result.current).toEqual(initialData); - }); + appMockRenderer = createAppMockRenderer(); }); it('calls getActionLicense with correct arguments', async () => { const spyOnGetActionLicense = jest.spyOn(api, 'getActionLicense'); - - await act(async () => { - const { waitForNextUpdate } = renderHook(() => - useGetActionLicense() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(spyOnGetActionLicense).toBeCalledWith(abortCtrl.signal); - }); - }); - - it('gets action license', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetActionLicense() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - isLoading: false, - isError: false, - actionLicense: actionLicenses[1], - }); + const { waitForNextUpdate } = renderHook(() => useGetActionLicense(), { + wrapper: appMockRenderer.AppWrapper, }); - }); - it('set isLoading to true when posting case', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetActionLicense() - ); - await waitForNextUpdate(); - expect(result.current.isLoading).toBe(true); - }); + await waitForNextUpdate(); + expect(spyOnGetActionLicense).toBeCalledWith(abortCtrl.signal); }); it('unhappy path', async () => { + const addError = jest.fn(); + + (useToasts as jest.Mock).mockReturnValue({ addError }); const spyOnGetActionLicense = jest.spyOn(api, 'getActionLicense'); spyOnGetActionLicense.mockImplementation(() => { throw new Error('Something went wrong'); }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetActionLicense() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - actionLicense: null, - isLoading: false, - isError: true, - }); + const { waitForNextUpdate } = renderHook(() => useGetActionLicense(), { + wrapper: appMockRenderer.AppWrapper, }); + await waitForNextUpdate(); + + expect(addError).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx index 7618f8c06d9ae..a64a449783ba9 100644 --- a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx @@ -5,81 +5,34 @@ * 2.0. */ -import { useCallback, useEffect, useState, useRef } from 'react'; - +import { useQuery } from 'react-query'; import { useToasts } from '../common/lib/kibana'; import { getActionLicense } from './api'; import * as i18n from './translations'; -import { ActionLicense } from './types'; import { ConnectorTypes } from '../../common/api'; - -export interface ActionLicenseState { - actionLicense: ActionLicense | null; - isLoading: boolean; - isError: boolean; -} - -export const initialData: ActionLicenseState = { - actionLicense: null, - isLoading: true, - isError: false, -}; +import { CASE_LICENSE_CACHE_KEY } from './constants'; +import { ServerError } from '../types'; const MINIMUM_LICENSE_REQUIRED_CONNECTOR = ConnectorTypes.jira; -export const useGetActionLicense = (): ActionLicenseState => { - const [actionLicenseState, setActionLicensesState] = useState(initialData); +export const useGetActionLicense = () => { const toasts = useToasts(); - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - - const fetchActionLicense = useCallback(async () => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - setActionLicensesState({ - ...initialData, - isLoading: true, - }); - - const response = await getActionLicense(abortCtrlRef.current.signal); - - if (!isCancelledRef.current) { - setActionLicensesState({ - actionLicense: response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!isCancelledRef.current) { + return useQuery( + [CASE_LICENSE_CACHE_KEY], + async () => { + const abortCtrl = new AbortController(); + const response = await getActionLicense(abortCtrl.signal); + return response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null; + }, + { + onError: (error: ServerError) => { if (error.name !== 'AbortError') { toasts.addError( error.body && error.body.message ? new Error(error.body.message) : error, { title: i18n.ERROR_TITLE } ); } - - setActionLicensesState({ - actionLicense: null, - isLoading: false, - isError: true, - }); - } + }, } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionLicenseState]); - - useEffect(() => { - fetchActionLicense(); - - return () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { ...actionLicenseState }; + ); }; diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx index 2607129e5655d..6a2a08132a02f 100644 --- a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx @@ -6,12 +6,12 @@ */ import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { useGetTags, UseGetTags } from './use_get_tags'; -import { tags } from './mock'; +import { renderHook } from '@testing-library/react-hooks'; import * as api from './api'; import { TestProviders } from '../common/mock'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; +import { useGetTags } from './use_get_tags'; +import { useToasts } from '../common/lib/kibana'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -23,77 +23,26 @@ describe('useGetTags', () => { jest.restoreAllMocks(); }); - it('init', async () => { - const { result } = renderHook(() => useGetTags(), { - wrapper: ({ children }) => {children}, - }); - - await act(async () => { - expect(result.current).toEqual({ - tags: [], - isLoading: true, - isError: false, - fetchTags: result.current.fetchTags, - }); - }); - }); - it('calls getTags api', async () => { const spyOnGetTags = jest.spyOn(api, 'getTags'); - await act(async () => { - const { waitForNextUpdate } = renderHook(() => useGetTags(), { - wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); - }); - }); - - it('fetch tags', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags(), { - wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - expect(result.current).toEqual({ - tags, - isLoading: false, - isError: false, - fetchTags: result.current.fetchTags, - }); - }); - }); - - it('refetch tags', async () => { - const spyOnGetTags = jest.spyOn(api, 'getTags'); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags(), { - wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - result.current.fetchTags(); - expect(spyOnGetTags).toHaveBeenCalledTimes(2); + const { waitForNextUpdate } = renderHook(() => useGetTags(), { + wrapper: ({ children }) => {children}, }); + await waitForNextUpdate(); + expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); }); - it('unhappy path', async () => { + it('displays and error toast when an error occurs', async () => { + const addError = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addError }); const spyOnGetTags = jest.spyOn(api, 'getTags'); spyOnGetTags.mockImplementation(() => { throw new Error('Something went wrong'); }); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags(), { - wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - tags: [], - isLoading: false, - isError: true, - fetchTags: result.current.fetchTags, - }); + const { waitForNextUpdate } = renderHook(() => useGetTags(), { + wrapper: ({ children }) => {children}, }); + await waitForNextUpdate(); + expect(addError).toBeCalled(); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.tsx index 15f3eafb2f113..3e18ac0a01315 100644 --- a/x-pack/plugins/cases/public/containers/use_get_tags.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_tags.tsx @@ -5,97 +5,33 @@ * 2.0. */ -import { useEffect, useReducer, useRef, useCallback } from 'react'; +import { useQuery } from 'react-query'; import { useToasts } from '../common/lib/kibana'; import { useCasesContext } from '../components/cases_context/use_cases_context'; +import { ServerError } from '../types'; import { getTags } from './api'; +import { CASE_TAGS_CACHE_KEY } from './constants'; import * as i18n from './translations'; -export interface TagsState { - tags: string[]; - isLoading: boolean; - isError: boolean; -} -type Action = - | { type: 'FETCH_INIT' } - | { type: 'FETCH_SUCCESS'; payload: string[] } - | { type: 'FETCH_FAILURE' }; - -export interface UseGetTags extends TagsState { - fetchTags: () => void; -} - -const dataFetchReducer = (state: TagsState, action: Action): TagsState => { - switch (action.type) { - case 'FETCH_INIT': - return { - ...state, - isLoading: true, - isError: false, - }; - case 'FETCH_SUCCESS': - return { - ...state, - isLoading: false, - isError: false, - tags: action.payload, - }; - case 'FETCH_FAILURE': - return { - ...state, - isLoading: false, - isError: true, - }; - default: - return state; - } -}; -const initialData: string[] = []; - -export const useGetTags = (): UseGetTags => { - const { owner } = useCasesContext(); - const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: true, - isError: false, - tags: initialData, - }); +export const useGetTags = (cacheKey?: string) => { const toasts = useToasts(); - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - - const callFetch = useCallback(async () => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - dispatch({ type: 'FETCH_INIT' }); - - const response = await getTags(abortCtrlRef.current.signal, owner); - - if (!isCancelledRef.current) { - dispatch({ type: 'FETCH_SUCCESS', payload: response }); - } - } catch (error) { - if (!isCancelledRef.current) { + const { owner } = useCasesContext(); + const key = [...(cacheKey ? [cacheKey] : []), CASE_TAGS_CACHE_KEY]; + return useQuery( + key, + () => { + const abortCtrl = new AbortController(); + return getTags(abortCtrl.signal, owner); + }, + { + onError: (error: ServerError) => { if (error.name !== 'AbortError') { toasts.addError( error.body && error.body.message ? new Error(error.body.message) : error, { title: i18n.ERROR_TITLE } ); } - dispatch({ type: 'FETCH_FAILURE' }); - } + }, } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - callFetch(); - return () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return { ...state, fetchTags: callFetch }; + ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_breadcrumbs.ts b/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_breadcrumbs.ts index 32ade5badafb0..89c77cf22ba64 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_breadcrumbs.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_breadcrumbs.ts @@ -9,6 +9,8 @@ import type { ChromeBreadcrumb, CoreStart } from '@kbn/core/public'; import { useEffect } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { type RouteProps, useRouteMatch, useHistory } from 'react-router-dom'; +import type { EuiBreadcrumb } from '@elastic/eui'; +import { string } from 'io-ts'; import type { CspNavigationItem } from './types'; import { CLOUD_POSTURE } from './translations'; @@ -27,7 +29,7 @@ const getClickableBreadcrumb = ( export const useCspBreadcrumbs = (breadcrumbs: CspNavigationItem[]) => { const { services: { - chrome: { setBreadcrumbs }, + chrome: { setBreadcrumbs, docTitle }, application: { getUrlForApp }, }, } = useKibana(); @@ -49,15 +51,21 @@ export const useCspBreadcrumbs = (breadcrumbs: CspNavigationItem[]) => { }; }); - setBreadcrumbs([ + const nextBreadcrumbs = [ { text: CLOUD_POSTURE, - onClick: (e) => { + onClick: (e: React.MouseEvent) => { e.preventDefault(); history.push(`/`); }, }, ...additionalBreadCrumbs, - ]); - }, [match.path, getUrlForApp, setBreadcrumbs, breadcrumbs, history]); + ]; + + setBreadcrumbs(nextBreadcrumbs); + docTitle.change(getTextBreadcrumbs(nextBreadcrumbs)); + }, [match.path, getUrlForApp, setBreadcrumbs, breadcrumbs, history, docTitle]); }; + +const getTextBreadcrumbs = (breadcrumbs: EuiBreadcrumb[]) => + breadcrumbs.map((breadcrumb) => breadcrumb.text).filter(string.is); 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 d13caa5e91543..4177f6d15c50c 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/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx index 9172872f839c9..fb74aba060892 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx @@ -19,7 +19,6 @@ import { encodeQuery } from '../../../common/navigation/query_utils'; import { useLocation } from 'react-router-dom'; import { RisonObject } from 'rison-node'; import { buildEsQuery } from '@kbn/es-query'; -import { getFindingsCountAggQuery } from '../use_findings_count'; import { getPaginationQuery } from '../utils'; import { FindingsEsPitContext } from '../es_pit/findings_es_pit_context'; @@ -38,7 +37,10 @@ beforeEach(() => { describe('', () => { it('data#search.search fn called with URL query', () => { - const query = getDefaultQuery(); + const query = getDefaultQuery({ + filters: [], + query: { language: 'kuery', query: '' }, + }); const dataMock = dataPluginMock.createStartContract(); const dataView = createStubDataView({ spec: { @@ -76,14 +78,11 @@ describe('', () => { }; expect(dataMock.search.search).toHaveBeenNthCalledWith(1, { - params: getFindingsCountAggQuery(baseQuery), - }); - - expect(dataMock.search.search).toHaveBeenNthCalledWith(2, { params: getFindingsQuery({ ...baseQuery, ...getPaginationQuery(query), sort: query.sort, + enabled: true, }), }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx index e5e72bf379734..b8e8dbb0001a3 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx @@ -4,29 +4,36 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { number } from 'io-ts'; +import { EuiSpacer } from '@elastic/eui'; import type { FindingsBaseProps } from '../types'; import { FindingsTable } from './latest_findings_table'; import { FindingsSearchBar } from '../layout/findings_search_bar'; import * as TEST_SUBJECTS from '../test_subjects'; -import { useUrlQuery } from '../../../common/hooks/use_url_query'; import { useLatestFindings } from './use_latest_findings'; import type { FindingsGroupByNoneQuery } from './use_latest_findings'; import type { FindingsBaseURLQuery } from '../types'; -import { useFindingsCounter } from '../use_findings_count'; import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; -import { getBaseQuery, getPaginationQuery, getPaginationTableParams } from '../utils'; +import { + getPaginationQuery, + getPaginationTableParams, + useBaseEsQuery, + usePersistedQuery, +} from '../utils'; import { PageWrapper, PageTitle, PageTitleText } from '../layout/findings_layout'; import { FindingsGroupBySelector } from '../layout/findings_group_by_selector'; import { useCspBreadcrumbs } from '../../../common/navigation/use_csp_breadcrumbs'; import { findingsNavigation } from '../../../common/navigation/constants'; +import { useUrlQuery } from '../../../common/hooks/use_url_query'; +import { ErrorCallout } from '../layout/error_callout'; -export const getDefaultQuery = (): FindingsBaseURLQuery & FindingsGroupByNoneQuery => ({ - query: { language: 'kuery', query: '' }, - filters: [], +export const getDefaultQuery = ({ + query, + filters, +}: FindingsBaseURLQuery): FindingsBaseURLQuery & FindingsGroupByNoneQuery => ({ + query, + filters, sort: { field: '@timestamp', direction: 'desc' }, pageIndex: 0, pageSize: 10, @@ -34,28 +41,30 @@ export const getDefaultQuery = (): FindingsBaseURLQuery & FindingsGroupByNoneQue export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { useCspBreadcrumbs([findingsNavigation.findings_default]); - const { urlQuery, setUrlQuery } = useUrlQuery(getDefaultQuery); - const baseEsQuery = useMemo( - () => getBaseQuery({ dataView, filters: urlQuery.filters, query: urlQuery.query }), - [dataView, urlQuery.filters, urlQuery.query] - ); + const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); + const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); - const findingsCount = useFindingsCounter(baseEsQuery); + /** + * Page URL query to ES query + */ + const baseEsQuery = useBaseEsQuery({ + dataView, + filters: urlQuery.filters, + query: urlQuery.query, + }); + + /** + * Page ES query result + */ const findingsGroupByNone = useLatestFindings({ - ...baseEsQuery, ...getPaginationQuery({ pageIndex: urlQuery.pageIndex, pageSize: urlQuery.pageSize }), + query: baseEsQuery.query, sort: urlQuery.sort, + enabled: !baseEsQuery.error, }); - const findingsDistribution = getFindingsDistribution({ - total: findingsGroupByNone.data?.total, - passed: findingsCount.data?.passed, - failed: findingsCount.data?.failed, - pageIndex: urlQuery.pageIndex, - pageSize: urlQuery.pageSize, - currentPageSize: findingsGroupByNone.data?.page.length, - }); + const error = findingsGroupByNone.error || baseEsQuery.error; return (
@@ -64,31 +73,50 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { setQuery={(query) => { setUrlQuery({ ...query, pageIndex: 0 }); }} - query={urlQuery.query} - filters={urlQuery.filters} - loading={findingsCount.isFetching} + loading={findingsGroupByNone.isFetching} /> - - {findingsDistribution && } - - - setUrlQuery({ pageIndex: page.index, pageSize: page.size, sort }) - } - /> + {error && } + {!error && ( + <> + + {findingsGroupByNone.isSuccess && ( + + )} + + + setUrlQuery({ + sort, + pageIndex: page.index, + pageSize: page.size, + }) + } + /> + + )}
); @@ -102,23 +130,11 @@ const LatestFindingsPageTitle = () => ( ); -const getFindingsDistribution = ({ - total, - passed, - failed, +const getFindingsPageSizeInfo = ({ currentPageSize, pageIndex, pageSize, -}: Record<'currentPageSize' | 'total' | 'passed' | 'failed', number | undefined> & - Record<'pageIndex' | 'pageSize', number>) => { - if (!number.is(total) || !number.is(passed) || !number.is(failed) || !number.is(currentPageSize)) - return; - - return { - total, - passed, - failed, - pageStart: pageIndex * pageSize + 1, - pageEnd: pageIndex * pageSize + currentPageSize, - }; -}; +}: Record<'pageIndex' | 'pageSize' | 'currentPageSize', number>) => ({ + pageStart: pageIndex * pageSize + 1, + pageEnd: pageIndex * pageSize + currentPageSize, +}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx index 550421902c472..a64e1bc30f851 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx @@ -68,8 +68,7 @@ describe('', () => { it('renders the zero state when status success and data has a length of zero ', async () => { const props: TableProps = { loading: false, - data: { page: [], total: 0 }, - error: null, + items: [], sorting: { sort: { field: '@timestamp', direction: 'desc' } }, pagination: { pageSize: 10, pageIndex: 1, totalItemCount: 0 }, setTableOptions: jest.fn(), @@ -90,8 +89,7 @@ describe('', () => { const props: TableProps = { loading: false, - data: { page: data, total: 10 }, - error: null, + items: data, sorting: { sort: { field: '@timestamp', direction: 'desc' } }, pagination: { pageSize: 10, pageIndex: 1, totalItemCount: 0 }, setTableOptions: jest.fn(), diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx index 784bd3fa50767..2140a4509fe15 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx @@ -14,32 +14,29 @@ import { type CriteriaWithPagination, type EuiTableActionsColumnType, } from '@elastic/eui'; -import { extractErrorMessage } from '../../../../common/utils/helpers'; import * as TEST_SUBJECTS from '../test_subjects'; import * as TEXT from '../translations'; import type { CspFinding } from '../types'; -import type { CspFindingsResult } from './use_latest_findings'; import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; import { getExpandColumn, getFindingsColumns } from '../layout/findings_layout'; type TableProps = Required>; -interface BaseFindingsTableProps { +interface Props { + loading: boolean; + items: CspFinding[]; pagination: Pagination; sorting: TableProps['sorting']; setTableOptions(options: CriteriaWithPagination): void; } -type FindingsTableProps = CspFindingsResult & BaseFindingsTableProps; - const FindingsTableComponent = ({ - error, - data, loading, + items, pagination, sorting, setTableOptions, -}: FindingsTableProps) => { +}: Props) => { const [selectedFinding, setSelectedFinding] = useState(); const columns: [ @@ -51,7 +48,7 @@ const FindingsTableComponent = ({ ); // Show "zero state" - if (!loading && !data?.page.length) + if (!loading && !items.length) // TODO: use our own logo return ( ; size: NonNullable; sort: Sort; + enabled: boolean; } type Sort = NonNullable['sort']>; @@ -35,7 +35,14 @@ export interface FindingsGroupByNoneQuery { sort: Sort; } -export type CspFindingsResult = FindingsQueryResult; +type LatestFindingsRequest = IKibanaSearchRequest; +type LatestFindingsResponse = IKibanaSearchResponse< + estypes.SearchResponse +>; + +interface FindingsAggs { + count: estypes.AggregationsMultiBucketAggregateBase; +} const FIELDS_WITHOUT_KEYWORD_MAPPING = new Set([ '@timestamp', @@ -63,42 +70,52 @@ export const getFindingsQuery = ({ sort, pitId, }: UseFindingsOptions & { pitId: string }) => ({ - query, - size, - from, - sort: [{ [getSortKey(sort.field)]: sort.direction }], + body: { + query, + sort: [{ [getSortKey(sort.field)]: sort.direction }], + size, + from, + aggs: { count: { terms: { field: 'result.evaluation.keyword' } } }, + }, pit: { id: pitId }, ignore_unavailable: false, }); -export const useLatestFindings = ({ query, sort, from, size }: UseFindingsOptions) => { +export const useLatestFindings = (options: UseFindingsOptions) => { const { data, notifications: { toasts }, } = useKibana().services; const { pitIdRef, setPitId } = useContext(FindingsEsPitContext); - const pitId = pitIdRef.current; - - return useQuery< - IEsSearchResponse, - unknown, - CspFindingsQueryData & { newPitId: string } - >( - ['csp_findings', { query, sort, from, size, pitId }], - () => - lastValueFrom>( - data.search.search({ - params: getFindingsQuery({ query, sort, from, size, pitId }), + const params = { ...options, pitId: pitIdRef.current }; + + return useQuery( + ['csp_findings', { params }], + async () => { + const { + rawResponse: { hits, aggregations, pit_id: newPitId }, + } = await lastValueFrom( + data.search.search({ + params: getFindingsQuery(params), }) - ), - { - keepPreviousData: true, - select: ({ rawResponse: { hits, pit_id: newPitId } }) => ({ + ); + + if (!aggregations) throw new Error('expected aggregations to be an defined'); + + if (!Array.isArray(aggregations.count.buckets)) + throw new Error('expected buckets to be an array'); + + return { page: hits.hits.map((hit) => hit._source!), total: number.is(hits.total) ? hits.total : 0, + count: getAggregationCount(aggregations.count.buckets), newPitId: newPitId!, - }), - onError: (err) => showErrorToast(toasts, err), + }; + }, + { + enabled: options.enabled, + keepPreviousData: true, + onError: (err: Error) => showErrorToast(toasts, err), onSuccess: ({ newPitId }) => { setPitId(newPitId); }, @@ -108,3 +125,13 @@ export const useLatestFindings = ({ query, sort, from, size }: UseFindingsOption } ); }; + +const getAggregationCount = (buckets: estypes.AggregationsStringRareTermsBucketKeys[]) => { + const passed = buckets.find((bucket) => bucket.key === 'passed'); + const failed = buckets.find((bucket) => bucket.key === 'failed'); + + return { + passed: passed?.doc_count || 0, + failed: failed?.doc_count || 0, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx index 726f727a7d933..3236b11be4ffa 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx @@ -13,16 +13,25 @@ import { useUrlQuery } from '../../../common/hooks/use_url_query'; import type { FindingsBaseProps, FindingsBaseURLQuery } from '../types'; import { FindingsByResourceQuery, useFindingsByResource } from './use_findings_by_resource'; import { FindingsByResourceTable } from './findings_by_resource_table'; -import { getBaseQuery, getPaginationQuery, getPaginationTableParams } from '../utils'; +import { + getPaginationQuery, + getPaginationTableParams, + useBaseEsQuery, + usePersistedQuery, +} from '../utils'; import { PageTitle, PageTitleText, PageWrapper } from '../layout/findings_layout'; import { FindingsGroupBySelector } from '../layout/findings_group_by_selector'; import { findingsNavigation } from '../../../common/navigation/constants'; import { useCspBreadcrumbs } from '../../../common/navigation/use_csp_breadcrumbs'; import { ResourceFindings } from './resource_findings/resource_findings_container'; +import { ErrorCallout } from '../layout/error_callout'; -const getDefaultQuery = (): FindingsBaseURLQuery & FindingsByResourceQuery => ({ - query: { language: 'kuery', query: '' }, - filters: [], +const getDefaultQuery = ({ + query, + filters, +}: FindingsBaseURLQuery): FindingsBaseURLQuery & FindingsByResourceQuery => ({ + query, + filters, pageIndex: 0, pageSize: 10, }); @@ -43,12 +52,30 @@ export const FindingsByResourceContainer = ({ dataView }: FindingsBaseProps) => const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { useCspBreadcrumbs([findingsNavigation.findings_by_resource]); - const { urlQuery, setUrlQuery } = useUrlQuery(getDefaultQuery); + + const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); + const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); + + /** + * Page URL query to ES query + */ + const baseEsQuery = useBaseEsQuery({ + dataView, + filters: urlQuery.filters, + query: urlQuery.query, + }); + + /** + * Page ES query result + */ const findingsGroupByResource = useFindingsByResource({ - ...getBaseQuery({ dataView, filters: urlQuery.filters, query: urlQuery.query }), ...getPaginationQuery(urlQuery), + query: baseEsQuery.query, + enabled: !baseEsQuery.error, }); + const error = findingsGroupByResource.error || baseEsQuery.error; + return (
{ setQuery={(query) => { setUrlQuery({ ...query, pageIndex: 0 }); }} - query={urlQuery.query} - filters={urlQuery.filters} - loading={findingsGroupByResource.isLoading} + loading={findingsGroupByResource.isFetching} /> @@ -71,20 +96,25 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { } /> - - - setUrlQuery({ pageIndex: page.index, pageSize: page.size }) - } - /> + {error && } + + {!error && ( + <> + + + setUrlQuery({ pageIndex: page.index, pageSize: page.size }) + } + /> + + )}
); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx index f607452cff589..6b712e6a83a51 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx @@ -7,21 +7,17 @@ import React from 'react'; import { render, screen, within } from '@testing-library/react'; import * as TEST_SUBJECTS from '../test_subjects'; -import { - FindingsByResourceTable, - formatNumber, - getResourceId, - type CspFindingsByResource, -} from './findings_by_resource_table'; +import { FindingsByResourceTable, formatNumber, getResourceId } from './findings_by_resource_table'; import * as TEXT from '../translations'; import type { PropsOf } from '@elastic/eui'; import Chance from 'chance'; import numeral from '@elastic/numeral'; import { TestProvider } from '../../../test/test_provider'; +import type { FindingsByResourcePage } from './use_findings_by_resource'; const chance = new Chance(); -const getFakeFindingsByResource = (): CspFindingsByResource => { +const getFakeFindingsByResource = (): FindingsByResourcePage => { const count = chance.integer(); const total = chance.integer() + count + 1; const normalized = count / total; @@ -46,8 +42,7 @@ describe('', () => { it('renders the zero state when status success and data has a length of zero ', async () => { const props: TableProps = { loading: false, - data: { page: [], total: 0 }, - error: null, + items: [], pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, setTableOptions: jest.fn(), }; @@ -66,8 +61,7 @@ describe('', () => { const props: TableProps = { loading: false, - data: { page: data, total: data.length }, - error: null, + items: data, pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, setTableOptions: jest.fn(), }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx index c7d53ba429ec2..641f1fb60ae90 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx @@ -19,46 +19,41 @@ import { FormattedMessage } from '@kbn/i18n-react'; import numeral from '@elastic/numeral'; import { Link, generatePath } from 'react-router-dom'; import { ColumnNameWithTooltip } from '../../../components/column_name_with_tooltip'; -import { extractErrorMessage } from '../../../../common/utils/helpers'; import * as TEST_SUBJECTS from '../test_subjects'; import * as TEXT from '../translations'; -import type { CspFindingsByResourceResult } from './use_findings_by_resource'; +import type { FindingsByResourcePage } from './use_findings_by_resource'; import { findingsNavigation } from '../../../common/navigation/constants'; export const formatNumber = (value: number) => value < 1000 ? value : numeral(value).format('0.0a'); -export type CspFindingsByResource = NonNullable< - CspFindingsByResourceResult['data'] ->['page'][number]; - -interface Props extends CspFindingsByResourceResult { +interface Props { + items: FindingsByResourcePage[]; + loading: boolean; pagination: Pagination; - setTableOptions(options: CriteriaWithPagination): void; + setTableOptions(options: CriteriaWithPagination): void; } -export const getResourceId = (resource: CspFindingsByResource) => +export const getResourceId = (resource: FindingsByResourcePage) => [resource.resource_id, ...resource.cis_sections].join('/'); const FindingsByResourceTableComponent = ({ - error, - data, + items, loading, pagination, setTableOptions, }: Props) => { - const getRowProps = (row: CspFindingsByResource) => ({ + const getRowProps = (row: FindingsByResourcePage) => ({ 'data-test-subj': TEST_SUBJECTS.getFindingsByResourceTableRowTestId(getResourceId(row)), }); - if (!loading && !data?.page.length) + if (!loading && !items.length) return {TEXT.NO_FINDINGS}} />; return ( > = [ +const columns: Array> = [ { field: 'resource_id', name: ( @@ -81,7 +76,7 @@ const columns: Array> = [ )} /> ), - render: (resourceId: CspFindingsByResource['resource_id']) => ( + render: (resourceId: FindingsByResourcePage['resource_id']) => ( {resourceId} @@ -143,7 +138,7 @@ const columns: Array> = [ defaultMessage="Failed Findings" /> ), - render: (failedFindings: CspFindingsByResource['failed_findings']) => ( + render: (failedFindings: FindingsByResourcePage['failed_findings']) => ( ({ - query: { language: 'kuery', query: '' }, - filters: [], +const getDefaultQuery = ({ + query, + filters, +}: FindingsBaseURLQuery): FindingsBaseURLQuery & ResourceFindingsQuery => ({ + query, + filters, pageIndex: 0, pageSize: 10, }); @@ -43,21 +52,34 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { useCspBreadcrumbs([findingsNavigation.findings_default]); const { euiTheme } = useEuiTheme(); const params = useParams<{ resourceId: string }>(); - const { urlQuery, setUrlQuery } = useUrlQuery(getDefaultQuery); + const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); + const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); + + /** + * Page URL query to ES query + */ + const baseEsQuery = useBaseEsQuery({ + dataView, + filters: urlQuery.filters, + query: urlQuery.query, + }); + + /** + * Page ES query result + */ const resourceFindings = useResourceFindings({ - resourceId: params.resourceId, - ...getBaseQuery({ - dataView, - filters: urlQuery.filters, - query: urlQuery.query, - }), ...getPaginationQuery({ pageSize: urlQuery.pageSize, pageIndex: urlQuery.pageIndex, }), + query: baseEsQuery.query, + resourceId: params.resourceId, + enabled: !baseEsQuery.error, }); + const error = resourceFindings.error || baseEsQuery.error; + return (
{ setQuery={(query) => { setUrlQuery({ ...query, pageIndex: 0 }); }} - query={urlQuery.query} - filters={urlQuery.filters} loading={resourceFindings.isFetching} /> @@ -85,19 +105,21 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { /> - - setUrlQuery({ pageIndex: page.index, pageSize: page.size }) - } - /> + {error && } + {!error && ( + + setUrlQuery({ pageIndex: page.index, pageSize: page.size }) + } + /> + )}
); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx index 3ed52499edbf1..d5fb62538b446 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx @@ -13,25 +13,19 @@ import { EuiBasicTableColumn, EuiTableActionsColumnType, } from '@elastic/eui'; -import { extractErrorMessage } from '../../../../../common/utils/helpers'; import * as TEXT from '../../translations'; -import type { ResourceFindingsResult } from './use_resource_findings'; import { getExpandColumn, getFindingsColumns } from '../../layout/findings_layout'; import type { CspFinding } from '../../types'; import { FindingsRuleFlyout } from '../../findings_flyout/findings_flyout'; -interface Props extends ResourceFindingsResult { +interface Props { + items: CspFinding[]; + loading: boolean; pagination: Pagination; setTableOptions(options: CriteriaWithPagination): void; } -const ResourceFindingsTableComponent = ({ - error, - data, - loading, - pagination, - setTableOptions, -}: Props) => { +const ResourceFindingsTableComponent = ({ items, loading, pagination, setTableOptions }: Props) => { const [selectedFinding, setSelectedFinding] = useState(); const columns: [ @@ -41,16 +35,14 @@ const ResourceFindingsTableComponent = ({ () => [getExpandColumn({ onClick: setSelectedFinding }), ...getFindingsColumns()], [] ); - - if (!loading && !data?.page.length) + if (!loading && !items.length) return {TEXT.NO_FINDINGS}} />; return ( <> ; size: NonNullable; + enabled: boolean; } export interface ResourceFindingsQuery { @@ -28,8 +29,6 @@ export interface ResourceFindingsQuery { pageSize: Pagination['pageSize']; } -export type ResourceFindingsResult = FindingsQueryResult; - const getResourceFindingsQuery = ({ query, resourceId, @@ -52,40 +51,32 @@ const getResourceFindingsQuery = ({ ignore_unavailable: false, }); -export const useResourceFindings = ({ - query, - resourceId, - from, - size, -}: UseResourceFindingsOptions) => { +export const useResourceFindings = (options: UseResourceFindingsOptions) => { const { data, notifications: { toasts }, } = useKibana().services; const { pitIdRef, setPitId } = useContext(FindingsEsPitContext); - const pitId = pitIdRef.current; + const params = { ...options, pitId: pitIdRef.current }; - return useQuery< - IEsSearchResponse, - unknown, - CspFindingsQueryData & { newPitId: string } - >( - ['csp_resource_findings', { query, resourceId, from, size, pitId }], + return useQuery( + ['csp_resource_findings', { params }], () => - lastValueFrom>( + lastValueFrom( data.search.search({ - params: getResourceFindingsQuery({ query, resourceId, from, size, pitId }), + params: getResourceFindingsQuery(params), }) ), { + enabled: options.enabled, keepPreviousData: true, - select: ({ rawResponse: { hits, pit_id: newPitId } }) => ({ + select: ({ rawResponse: { hits, pit_id: newPitId } }: IEsSearchResponse) => ({ page: hits.hits.map((hit) => hit._source!), - total: hits.total as number, + total: number.is(hits.total) ? hits.total : 0, newPitId: newPitId!, }), - onError: (err) => showErrorToast(toasts, err), + onError: (err: Error) => showErrorToast(toasts, err), onSuccess: ({ newPitId }) => { setPitId(newPitId); }, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts index 9201a0520d12f..dc83230c3217b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts @@ -14,7 +14,13 @@ import { FindingsEsPitContext } from '../es_pit/findings_es_pit_context'; import { FINDINGS_REFETCH_INTERVAL_MS } from '../constants'; import { useKibana } from '../../../common/hooks/use_kibana'; import { showErrorToast } from '../latest_findings/use_latest_findings'; -import type { FindingsBaseEsQuery, FindingsQueryResult } from '../types'; +import type { FindingsBaseEsQuery } from '../types'; + +interface UseFindingsByResourceOptions extends FindingsBaseEsQuery { + from: NonNullable; + size: NonNullable; + enabled: boolean; +} // Maximum number of grouped findings, default limit in elasticsearch is set to 65,536 (ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-settings.html#search-settings-max-buckets) const MAX_BUCKETS = 60 * 1000; @@ -34,7 +40,7 @@ type FindingsAggResponse = IKibanaSearchResponse< estypes.SearchResponse<{}, FindingsByResourceAggs> >; -interface FindingsByResourcePage { +export interface FindingsByResourcePage { failed_findings: { count: number; normalized: number; @@ -47,16 +53,6 @@ interface FindingsByResourcePage { cis_sections: string[]; } -interface UseFindingsByResourceData { - page: FindingsByResourcePage[]; - total: number; -} - -export type CspFindingsByResourceResult = FindingsQueryResult< - UseFindingsByResourceData | undefined, - unknown ->; - interface FindingsByResourceAggs { resource_total: estypes.AggregationsCardinalityAggregate; resources: estypes.AggregationsMultiBucketAggregateBase; @@ -120,37 +116,41 @@ export const getFindingsByResourceAggQuery = ({ ignore_unavailable: false, }); -export const useFindingsByResource = ({ query, from, size }: UseResourceFindingsOptions) => { +export const useFindingsByResource = (options: UseFindingsByResourceOptions) => { const { data, notifications: { toasts }, } = useKibana().services; const { pitIdRef, setPitId } = useContext(FindingsEsPitContext); - const pitId = pitIdRef.current; - - return useQuery( - ['csp_findings_resource', { query, size, from, pitId }], - () => - lastValueFrom( + const params = { ...options, pitId: pitIdRef.current }; + + return useQuery( + ['csp_findings_resource', { params }], + async () => { + const { + rawResponse: { aggregations, pit_id: newPitId }, + } = await lastValueFrom( data.search.search({ - params: getFindingsByResourceAggQuery({ query, from, size, pitId }), + params: getFindingsByResourceAggQuery(params), }) - ).then(({ rawResponse: { aggregations, pit_id: newPitId } }) => { - if (!aggregations) throw new Error('expected aggregations to be defined'); - - if (!Array.isArray(aggregations.resources.buckets)) - throw new Error('expected resources buckets to be an array'); - - return { - page: aggregations.resources.buckets.map(createFindingsByResource), - total: aggregations.resource_total.value, - newPitId: newPitId!, - }; - }), + ); + + if (!aggregations) throw new Error('expected aggregations to be defined'); + + if (!Array.isArray(aggregations.resources.buckets)) + throw new Error('expected buckets to be an array'); + + return { + page: aggregations.resources.buckets.map(createFindingsByResource), + total: aggregations.resource_total.value, + newPitId: newPitId!, + }; + }, { + enabled: options.enabled, keepPreviousData: true, - onError: (err) => showErrorToast(toasts, err), + onError: (err: Error) => showErrorToast(toasts, err), onSuccess: ({ newPitId }) => { setPitId(newPitId); }, @@ -176,9 +176,9 @@ const createFindingsByResource = (resource: FindingsAggBucket): FindingsByResour return { resource_id: resource.key, - resource_name: resource.name.buckets[0].key, - resource_subtype: resource.subtype.buckets[0].key, - cluster_id: resource.cluster_id.buckets[0].key, + resource_name: resource.name.buckets[0]?.key, + resource_subtype: resource.subtype.buckets[0]?.key, + cluster_id: resource.cluster_id.buckets[0]?.key, cis_sections: resource.cis_sections.buckets.map((v) => v.key), failed_findings: { count: resource.failed_findings.doc_count, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/error_callout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/error_callout.tsx new file mode 100644 index 0000000000000..8155a4cb59537 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/error_callout.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiCallOut, + EuiSpacer, + EuiButton, +} from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { PAGE_SEARCH_ERROR_MESSAGE } from '../translations'; +import { useKibana } from '../../../common/hooks/use_kibana'; + +export const ErrorCallout = ({ error }: { error: Error }) => { + const { + data: { search }, + } = useKibana().services; + + return ( + + + + + + search.showError(error)}> + + + + + + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx index c155b1cae7eda..d1eb97832ab7c 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx @@ -9,8 +9,8 @@ import { css } from '@emotion/react'; import { EuiHealth, EuiBadge, - EuiTextColor, EuiSpacer, + EuiTextColor, EuiFlexGroup, EuiFlexItem, useEuiTheme, @@ -33,21 +33,21 @@ export const FindingsDistributionBar = (props: Props) => (
- + {}
); -const Counters = ({ pageStart, pageEnd, total, failed, passed }: Props) => ( +const Counters = (props: Props) => ( - + - + ); @@ -100,6 +100,7 @@ const CurrentPageOfTotal = ({ const DistributionBar: React.FC> = ({ passed, failed }) => { const { euiTheme } = useEuiTheme(); + return ( ; -interface FindingsSearchBarProps extends SearchBarQueryProps { +interface FindingsSearchBarProps { setQuery(v: Partial): void; loading: boolean; } export const FindingsSearchBar = ({ dataView, - query, - filters, loading, setQuery, }: FindingsSearchBarProps & { dataView: DataView }) => { @@ -48,8 +46,6 @@ export const FindingsSearchBar = ({ showSaveQuery={false} isLoading={loading} indexPatterns={[dataView]} - query={query} - filters={filters} onQuerySubmit={setQuery} // @ts-expect-error onFiltersUpdated is a valid prop on SearchBar onFiltersUpdated={(value: Filter[]) => setQuery({ filters: value })} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts index a2365afb0ddc6..32ad3207405dd 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts @@ -268,3 +268,8 @@ export const FINDINGS_SEARCH_PLACEHOLDER = i18n.translate( 'xpack.csp.findings.searchBar.searchPlaceholder', { defaultMessage: 'Search findings (eg. rule.section.keyword : "API Server" )' } ); + +export const PAGE_SEARCH_ERROR_MESSAGE = i18n.translate( + 'xpack.csp.findings.errorCallout.pageSearchErrorTitle', + { defaultMessage: 'We encountered an error retrieving search results' } +); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts index 4f562b638f6d1..4f984c5a1f1e3 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts @@ -6,7 +6,6 @@ */ import type { DataView } from '@kbn/data-views-plugin/common'; import type { BoolQuery, Filter, Query } from '@kbn/es-query'; -import type { UseQueryResult } from 'react-query'; export type FindingsGroupByKind = 'default' | 'resource'; @@ -25,12 +24,6 @@ export interface FindingsBaseEsQuery { }; } -export interface FindingsQueryResult { - loading: UseQueryResult['isLoading']; - error: TError; - data: TData; -} - // TODO: this needs to be defined in a versioned schema export interface CspFinding { '@timestamp': string; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/use_findings_count.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/use_findings_count.ts deleted file mode 100644 index 051ab68f77c62..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/use_findings_count.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useQuery } from 'react-query'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { lastValueFrom } from 'rxjs'; -import type { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/public'; -import { useContext } from 'react'; -import { useKibana } from '../../common/hooks/use_kibana'; -import { showErrorToast } from './latest_findings/use_latest_findings'; -import type { FindingsBaseEsQuery } from './types'; -import { FindingsEsPitContext } from './es_pit/findings_es_pit_context'; - -type FindingsAggRequest = IKibanaSearchRequest; -type FindingsAggResponse = IKibanaSearchResponse>; -interface FindingsAggs extends estypes.AggregationsMultiBucketAggregateBase { - count: { - buckets: Array<{ - key: string; - doc_count: number; - }>; - }; -} - -interface UseFindingsCounterData { - passed: number; - failed: number; -} - -export const getFindingsCountAggQuery = ({ - query, - pitId, -}: FindingsBaseEsQuery & { pitId: string }) => ({ - size: 0, - track_total_hits: true, - body: { - query, - aggs: { count: { terms: { field: 'result.evaluation.keyword' } } }, - pit: { id: pitId }, - }, - ignore_unavailable: false, -}); - -export const useFindingsCounter = ({ query }: FindingsBaseEsQuery) => { - const { - data, - notifications: { toasts }, - } = useKibana().services; - - const { pitIdRef, setPitId } = useContext(FindingsEsPitContext); - const pitId = pitIdRef.current; - - return useQuery( - ['csp_findings_counts', { query, pitId }], - () => - lastValueFrom( - data.search.search({ - params: getFindingsCountAggQuery({ query, pitId }), - }) - ), - { - keepPreviousData: true, - onError: (err) => showErrorToast(toasts, err), - select: (response) => ({ - ...(Object.fromEntries( - response.rawResponse.aggregations!.count.buckets.map((bucket) => [ - bucket.key, - bucket.doc_count, - ])! - ) as { passed: number; failed: number }), - newPitId: response.rawResponse.pit_id!, - }), - onSuccess: ({ newPitId }) => { - setPitId(newPitId); - }, - } - ); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts index 6120085c179d7..d6ed2e29de616 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts @@ -7,19 +7,23 @@ import { buildEsQuery } from '@kbn/es-query'; import { EuiBasicTableProps, Pagination } from '@elastic/eui'; -import { FindingsBaseProps } from './types'; -import type { FindingsBaseEsQuery, FindingsBaseURLQuery } from './types'; +import { useCallback, useEffect, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { FindingsBaseProps, FindingsBaseURLQuery } from './types'; +import { useKibana } from '../../common/hooks/use_kibana'; -export const getBaseQuery = ({ - dataView, - query, - filters, -}: FindingsBaseURLQuery & FindingsBaseProps): FindingsBaseEsQuery => ({ - // TODO: this will throw for malformed query - // page will display an error boundary with the JS error - // will be accounted for before releasing the feature - query: buildEsQuery(dataView, query, filters), -}); +const getBaseQuery = ({ dataView, query, filters }: FindingsBaseURLQuery & FindingsBaseProps) => { + try { + return { + query: buildEsQuery(dataView, query, filters), // will throw for malformed query + }; + } catch (error) { + return { + query: undefined, + error: error instanceof Error ? error : new Error('Unknown Error'), + }; + } +}; type TablePagination = NonNullable['pagination']>; @@ -33,6 +37,23 @@ export const getPaginationTableParams = ( showPerPageOptions, }); +export const usePersistedQuery = (getter: ({ filters, query }: FindingsBaseURLQuery) => T) => { + const { + data: { + query: { filterManager, queryString }, + }, + } = useKibana().services; + + return useCallback( + () => + getter({ + filters: filterManager.getAppFilters(), + query: queryString.getQuery(), + }), + [getter, filterManager, queryString] + ); +}; + export const getPaginationQuery = ({ pageIndex, pageSize, @@ -40,3 +61,45 @@ export const getPaginationQuery = ({ from: pageIndex * pageSize, size: pageSize, }); + +export const useBaseEsQuery = ({ + dataView, + filters, + query, +}: FindingsBaseURLQuery & FindingsBaseProps) => { + const { + notifications: { toasts }, + data: { + query: { filterManager, queryString }, + }, + } = useKibana().services; + + const baseEsQuery = useMemo( + () => getBaseQuery({ dataView, filters, query }), + [dataView, filters, query] + ); + + /** + * Sync filters with the URL query + */ + useEffect(() => { + filterManager.setAppFilters(filters); + queryString.setQuery(query); + }, [filters, filterManager, queryString, query]); + + const handleMalformedQueryError = () => { + const error = baseEsQuery.error; + if (error) { + toasts.addError(error, { + title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', { + defaultMessage: 'Query Error', + }), + toastLifeTimeMs: 1000 * 5, + }); + } + }; + + useEffect(handleMalformedQueryError, [baseEsQuery.error, toasts]); + + return baseEsQuery; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx index bf109893aa1a4..f9f8eddc623c0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/index.tsx @@ -11,6 +11,7 @@ import { EuiTextColor, EuiEmptyPrompt, EuiButtonEmpty, EuiFlexGroup } from '@ela import * as t from 'io-ts'; import type { KibanaPageTemplateProps } from '@kbn/kibana-react-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; +import { pagePathGetters } from '@kbn/fleet-plugin/public'; import { RulesContainer, type PageUrlParams } from './rules_container'; import { allNavigationItems } from '../../common/navigation/constants'; import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs'; @@ -18,6 +19,7 @@ import { CspNavigationItem } from '../../common/navigation/types'; import { extractErrorMessage } from '../../../common/utils/helpers'; import { useCspIntegration } from './use_csp_integration'; import { CspPageTemplate } from '../../components/csp_page_template'; +import { useKibana } from '../../common/hooks/use_kibana'; const getRulesBreadcrumbs = (name?: string): CspNavigationItem[] => [allNavigationItems.benchmarks, { ...allNavigationItems.rules, name }].filter( @@ -25,6 +27,7 @@ const getRulesBreadcrumbs = (name?: string): CspNavigationItem[] => ); export const Rules = ({ match: { params } }: RouteComponentProps) => { + const { http } = useKibana().services; const integrationInfo = useCspIntegration(params); const breadcrumbs = useMemo( // TODO: make benchmark breadcrumb navigable @@ -37,6 +40,19 @@ export const Rules = ({ match: { params } }: RouteComponentProps) const pageProps: KibanaPageTemplateProps = useMemo( () => ({ pageHeader: { + alignItems: 'bottom', + rightSideItems: [ + + + , + ], pageTitle: ( @@ -70,7 +86,7 @@ export const Rules = ({ match: { params } }: RouteComponentProps) ), }, }), - [integrationInfo.data] + [http.basePath, integrationInfo.data, params] ); return ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index 24bbac090426a..797f8c0796568 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -5,17 +5,8 @@ * 2.0. */ import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - type EuiBasicTable, - EuiPanel, - EuiSpacer, -} from '@elastic/eui'; +import { type EuiBasicTable, EuiPanel, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { useParams } from 'react-router-dom'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { pagePathGetters } from '@kbn/fleet-plugin/public'; import { cspRuleAssetSavedObjectType } from '../../../common/constants'; import { extractErrorMessage, isNonNullable } from '../../../common/utils/helpers'; import { RulesTable } from './rules_table'; @@ -30,7 +21,7 @@ import { } from './use_csp_rules'; import * as TEST_SUBJECTS from './test_subjects'; import { RuleFlyout } from './rules_flyout'; -import { useKibana } from '../../common/hooks/use_kibana'; +import { DATA_UPDATE_INFO } from './translations'; interface RulesPageData { rules_page: RuleSavedObject[]; @@ -178,7 +169,7 @@ export const RulesContainer = () => { return (
- + {
); }; - -const ManageIntegrationButton = ({ policyId, packagePolicyId }: PageUrlParams) => { - const { http } = useKibana().services; - return ( - - - - - - - - ); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx index e7ac8e84b7424..d372691c8894e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx @@ -45,7 +45,6 @@ export const RuleFlyout = ({ onClose, rule, toggleRule }: RuleFlyoutProps) => { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/translations.ts b/x-pack/plugins/cloud_security_posture/public/pages/rules/translations.ts index 63fc5d1736644..f7f5e9b4e5601 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/translations.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/translations.ts @@ -84,3 +84,8 @@ export const OVERVIEW = i18n.translate('xpack.csp.rules.ruleFlyout.tabs.overview export const REMEDIATION = i18n.translate('xpack.csp.rules.ruleFlyout.tabs.remediationTabLabel', { defaultMessage: 'Remediation', }); + +export const DATA_UPDATE_INFO = i18n.translate('xpack.csp.rules.dataUpdateInfoCallout', { + defaultMessage: + 'Please note, any changes to your benchmark rules will take effect the next time your resources are evaluated. This can take up to ~5 hours', +}); diff --git a/x-pack/plugins/cloud_security_posture/server/create_transforms/latest_findings_transform.ts b/x-pack/plugins/cloud_security_posture/server/create_transforms/latest_findings_transform.ts index fb8027a7675d2..38247357990d6 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_transforms/latest_findings_transform.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_transforms/latest_findings_transform.ts @@ -26,7 +26,7 @@ export const latestFindingsTransform: TransformPutTransformRequest = { retention_policy: { time: { field: '@timestamp', - max_age: '3d', + max_age: '5h', }, }, latest: { diff --git a/x-pack/plugins/cloud_security_posture/server/lib/task_manager_util.ts b/x-pack/plugins/cloud_security_posture/server/lib/task_manager_util.ts index d03e754956891..190ba8aa7e2a2 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/task_manager_util.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/task_manager_util.ts @@ -20,7 +20,7 @@ export async function scheduleTaskSafe( ): Promise { try { await taskManager.ensureScheduled(taskConfig); - logger.info(`task: ${taskConfig.id} is scheduled`); + logger.info(`Task: ${taskConfig.id} is scheduled`); } catch (errMsg) { const error = transformError(errMsg); logger.error(`Error scheduling task, received ${error.message}`); diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.test.ts b/x-pack/plugins/cloud_security_posture/server/plugin.test.ts index e104001d65b4d..3236bf7cd6c8a 100644 --- a/x-pack/plugins/cloud_security_posture/server/plugin.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.test.ts @@ -15,7 +15,7 @@ import { } from '@kbn/fleet-plugin/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; +import { createPackagePolicyMock, deletePackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; import { CspPlugin } from './plugin'; import { CspServerPluginStartDeps } from './types'; @@ -28,12 +28,13 @@ import { import { ExternalCallback, FleetStartContract, + PostPackagePolicyDeleteCallback, PostPackagePolicyPostCreateCallback, } from '@kbn/fleet-plugin/server'; import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants'; import Chance from 'chance'; import type { AwaitedProperties } from '@kbn/utility-types'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { ElasticsearchClient, RequestHandlerContext, @@ -264,5 +265,63 @@ describe('Cloud Security Posture Plugin', () => { } expect(fleetMock.packagePolicyService.update).toHaveBeenCalledTimes(1); }); + + it('should uninstall resources when package is removed', async () => { + fleetMock.packageService.asInternalUser.getInstallation.mockImplementationOnce( + async (): Promise => { + return; + } + ); + + const deletedPackagePolicyMock = deletePackagePolicyMock(); + deletedPackagePolicyMock[0].package!.name = CLOUD_SECURITY_POSTURE_PACKAGE_NAME; + + const packagePolicyPostDeleteCallbacks: PostPackagePolicyDeleteCallback[] = []; + fleetMock.registerExternalCallback.mockImplementation((...args) => { + if (args[0] === 'postPackagePolicyDelete') { + packagePolicyPostDeleteCallbacks.push(args[1]); + } + }); + + const coreStart = coreMock.createStart(); + const repositoryFindMock = coreStart.savedObjects.createInternalRepository() + .find as jest.Mock; + + repositoryFindMock.mockReturnValueOnce( + Promise.resolve({ + saved_objects: [ + { + type: 'csp-rule-template', + id: 'csp_rule_template-41308bcdaaf665761478bb6f0d745a5c', + }, + ], + }) + ); + + repositoryFindMock.mockReturnValueOnce( + Promise.resolve({ + saved_objects: [], + }) + ); + + const context = coreMock.createPluginInitializerContext(); + plugin = new CspPlugin(context); + const spy = jest.spyOn(plugin, 'uninstallResources').mockImplementation(); + + // Act + await plugin.start(coreStart, mockPlugins); + await mockPlugins.fleet.fleetSetupCompleted(); + + // Assert + expect(fleetMock.packageService.asInternalUser.getInstallation).toHaveBeenCalledTimes(1); + + expect(packagePolicyPostDeleteCallbacks.length).toBeGreaterThan(0); + + for (const cb of packagePolicyPostDeleteCallbacks) { + await cb(deletedPackagePolicyMock); + } + expect(repositoryFindMock).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts index 5f625c3b2c6b1..bbc7da0fe22ae 100644 --- a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts +++ b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts @@ -28,7 +28,7 @@ import { scheduleTaskSafe, removeTaskSafe } from '../lib/task_manager_util'; import { CspServerPluginStartServices } from '../types'; const CSPM_FINDINGS_STATS_TASK_ID = 'cloud_security_posture-findings_stats'; -const CSPM_FINDINGS_STATS_TASK_TYPE = 'cloud_security_posture-findings_stats'; +const CSPM_FINDINGS_STATS_TASK_TYPE = 'cloud_security_posture-findings'; const CSPM_FINDINGS_STATS_INTERVAL = '5m'; export async function scheduleFindingsStatsTask( @@ -69,7 +69,7 @@ export function setupFindingsStatsTask( createTaskRunner: taskRunner(coreStartServices, logger), }, }); - logger.info(`task: ${CSPM_FINDINGS_STATS_TASK_TYPE} registered successfully`); + logger.info(`Task: ${CSPM_FINDINGS_STATS_TASK_TYPE} registered successfully`); } catch (errMsg) { const error = transformError(errMsg); logger.error(`Failed to register task: ${CSPM_FINDINGS_STATS_TASK_TYPE}, ${error.message}`); diff --git a/x-pack/plugins/dashboard_enhanced/tsconfig.json b/x-pack/plugins/dashboard_enhanced/tsconfig.json index cebf306003aba..9cd81c66fff4b 100644 --- a/x-pack/plugins/dashboard_enhanced/tsconfig.json +++ b/x-pack/plugins/dashboard_enhanced/tsconfig.json @@ -18,7 +18,7 @@ { "path": "../../../src/plugins/share/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json"}, { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../embeddable_enhanced/tsconfig.json" }, - { "path": "../ui_actions_enhanced/tsconfig.json" }, ] } diff --git a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json index 419c3875435dc..b3ae397963c1d 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json +++ b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json @@ -9,7 +9,7 @@ "include": ["public/**/*"], "references": [ { "path": "../../../../src/core/tsconfig.json" }, - { "path": "../../ui_actions_enhanced/tsconfig.json" }, + { "path": "../../../../src/plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../../src/plugins/expressions/tsconfig.json" }, { "path": "../../../../src/plugins/kibana_react/tsconfig.json" }, diff --git a/x-pack/plugins/embeddable_enhanced/tsconfig.json b/x-pack/plugins/embeddable_enhanced/tsconfig.json index f9ac369587473..13e684dbdefce 100644 --- a/x-pack/plugins/embeddable_enhanced/tsconfig.json +++ b/x-pack/plugins/embeddable_enhanced/tsconfig.json @@ -14,7 +14,6 @@ { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, - - { "path": "../ui_actions_enhanced/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions_enhanced/tsconfig.json" }, ] } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx index 20aaa6c76e276..3a1e52e8d8afd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx @@ -156,30 +156,33 @@ export const ProductSelector: React.FC = ({ - - - - - -

- {i18n.translate('xpack.enterpriseSearch.overview.heading', { - defaultMessage: 'Welcome to Elastic Enterprise Search', - })} -

-

- {config.host - ? i18n.translate('xpack.enterpriseSearch.overview.subheading', { - defaultMessage: 'Add search to your app or organization.', - }) - : i18n.translate('xpack.enterpriseSearch.overview.setupHeading', { - defaultMessage: 'Choose a product to set up and get started.', + + + + + + +

+ {i18n.translate('xpack.enterpriseSearch.overview.heading', { + defaultMessage: 'Welcome to Elastic Enterprise Search', })} -

- +

+

+ {config.host + ? i18n.translate('xpack.enterpriseSearch.overview.subheading', { + defaultMessage: 'Add search to your app or organization.', + }) + : i18n.translate('xpack.enterpriseSearch.overview.setupHeading', { + defaultMessage: 'Choose a product to set up and get started.', + })} +

+
+ +
{shouldShowEnterpriseSearchCards ? productCards : insufficientAccessMessage} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx index c4f97eb718452..ad068c29cb0e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx @@ -30,7 +30,11 @@ export const SourceAdded: React.FC = () => { const state = JSON.parse(params.state); const isOrganization = state.context !== 'account'; const { setChromeIsVisible } = useValues(KibanaLogic); - const { saveSourceParams } = useActions(AddSourceLogic); + const addSourceLogic = AddSourceLogic({ + serviceType: state.service_type, + initialStep: 'configure', + }); + const { saveSourceParams } = useActions(addSourceLogic); // We don't want the personal dashboard to flash the Kibana chrome, so we hide it. setChromeIsVisible(isOrganization); diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 9d84b165cfbae..ee76180f6da54 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -24,7 +24,6 @@ import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/p import { APP_SEARCH_PLUGIN, ELASTICSEARCH_PLUGIN, - ENTERPRISE_SEARCH_CONTENT_PLUGIN, ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; @@ -90,29 +89,6 @@ export class EnterpriseSearchPlugin implements Plugin { }, }); - core.application.register({ - id: ENTERPRISE_SEARCH_CONTENT_PLUGIN.ID, - title: ENTERPRISE_SEARCH_CONTENT_PLUGIN.NAV_TITLE, - euiIconType: ENTERPRISE_SEARCH_CONTENT_PLUGIN.LOGO, - appRoute: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL, - category: DEFAULT_APP_CATEGORIES.enterpriseSearch, - mount: async (params: AppMountParameters) => { - const kibanaDeps = await this.getKibanaDeps(core, params, cloud); - const { chrome, http } = kibanaDeps.core; - chrome.docTitle.change(ENTERPRISE_SEARCH_CONTENT_PLUGIN.NAME); - - await this.getInitialData(http); - const pluginData = this.getPluginData(); - - const { renderApp } = await import('./applications'); - const { EnterpriseSearchContent } = await import( - './applications/enterprise_search_content' - ); - - return renderApp(EnterpriseSearchContent, kibanaDeps, pluginData); - }, - }); - core.application.register({ id: ELASTICSEARCH_PLUGIN.ID, title: ELASTICSEARCH_PLUGIN.NAME, diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index c3dbfd99359de..89e849127cd24 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -24,7 +24,6 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, - ENTERPRISE_SEARCH_CONTENT_PLUGIN, ELASTICSEARCH_PLUGIN, APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, @@ -93,7 +92,6 @@ export class EnterpriseSearchPlugin implements Plugin { const log = this.logger; const PLUGIN_IDS = [ ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, - ENTERPRISE_SEARCH_CONTENT_PLUGIN.ID, ELASTICSEARCH_PLUGIN.ID, APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID, diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md index 0ec54b322baae..20433cf72b5c1 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/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index ae5867524cd79..9fd95d6843e12 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -12,7 +12,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; * This object is then used to validate and parse the value entered. */ export const allowedExperimentalValues = Object.freeze({ - createPackagePolicyMultiPageLayout: false, + createPackagePolicyMultiPageLayout: true, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index fee6f4c2ae4c4..f6c72ae9d2680 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -99,7 +99,7 @@ export interface CurrentUpgrade { nbAgents: number; nbAgentsAck: number; version: string; - startTime: string; + startTime?: string; } // Generated from FleetServer schema.json diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index cb5d8f3bb009b..902b32745d0e6 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -149,7 +149,7 @@ interface RegistryOverridePropertyValue { screenshots?: RegistryImage[]; } -export type RegistryRelease = PackageSpecManifest['release']; +export type RegistryRelease = NonNullable; export interface RegistryImage extends PackageSpecIcon { path: string; } @@ -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/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts index f4021b087912a..9eadbb65f373c 100644 --- a/x-pack/plugins/fleet/common/types/models/package_spec.ts +++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts @@ -16,7 +16,7 @@ export interface PackageSpecManifest { version: string; license?: 'basic'; type?: 'integration'; - release: 'experimental' | 'beta' | 'ga'; + release?: 'experimental' | 'beta' | 'ga'; categories?: Array; conditions?: PackageSpecConditions; icons?: PackageSpecIcon[]; diff --git a/x-pack/plugins/fleet/cypress/integration/agent.spec.ts b/x-pack/plugins/fleet/cypress/integration/agent.spec.ts index defe87fe89aa1..3881a66d8b603 100644 --- a/x-pack/plugins/fleet/cypress/integration/agent.spec.ts +++ b/x-pack/plugins/fleet/cypress/integration/agent.spec.ts @@ -66,6 +66,7 @@ const createAgentDoc = ( const createAgentDocs = (kibanaVersion: string) => [ createAgentDoc('agent-1', 'policy-1'), // this agent will have upgrade available createAgentDoc('agent-2', 'policy-2', 'error', kibanaVersion), + ...[...Array(15).keys()].map((_, index) => createAgentDoc(`agent-${index + 2}`, 'policy-3')), ]; let docs: any[] = []; @@ -116,6 +117,14 @@ describe('View agents', () => { monitoring_enabled: ['logs', 'metrics'], status: 'active', }, + { + id: 'policy-4', + name: 'Agent policy 4', + description: '', + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + status: 'active', + }, ], }); }); @@ -123,7 +132,7 @@ describe('View agents', () => { describe('Agent filter suggestions', () => { it('should filter based on agent id', () => { cy.visit('/app/fleet/agents'); - cy.getBySel('agentList.queryInput').type('agent.id: agent-1{enter}'); + cy.getBySel('agentList.queryInput').type('agent.id: "agent-1"{enter}'); cy.getBySel('fleetAgentListTable'); cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 2); cy.getBySel('fleetAgentListTable').contains('agent-1'); @@ -135,7 +144,7 @@ describe('View agents', () => { cy.visit('/app/fleet/agents'); cy.getBySel('agentList.showUpgradeable').click(); - cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 2); + cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 17); cy.getBySel('fleetAgentListTable').contains('agent-1'); }); @@ -144,7 +153,7 @@ describe('View agents', () => { cy.getBySel('agentList.showUpgradeable').click(); cy.getBySel('agentList.showUpgradeable').click(); - cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 3); + cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 18); cy.getBySel('fleetAgentListTable').contains('agent-1'); cy.getBySel('fleetAgentListTable').contains('agent-2'); }); @@ -165,7 +174,7 @@ describe('View agents', () => { cy.getBySel('agentList.policyFilter').click(); - cy.get('button').contains('Agent policy 3').click(); + cy.get('button').contains('Agent policy 4').click(); cy.getBySel('fleetAgentListTable').contains('No agents found'); }); @@ -193,14 +202,14 @@ describe('View agents', () => { }); }); describe('Agent status filter', () => { - it('should filter on healthy (1 result)', () => { + it('should filter on healthy (16 result)', () => { cy.visit('/app/fleet/agents'); cy.getBySel('agentList.statusFilter').click(); cy.get('button').contains('Healthy').click(); - cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 2); + cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 17); cy.getBySel('fleetAgentListTable').contains('agent-1'); }); it('should filter on unhealthy (1 result)', () => { @@ -230,9 +239,29 @@ describe('View agents', () => { cy.get('button').contains('healthy').click(); cy.get('button').contains('Unhealthy').click(); - cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 3); + cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 18); cy.getBySel('fleetAgentListTable').contains('agent-1'); cy.getBySel('fleetAgentListTable').contains('agent-2'); }); }); + + describe('Bulk actions', () => { + it('should allow to bulk upgrade agents', () => { + cy.visit('/app/fleet/agents'); + + cy.getBySel('agentList.policyFilter').click(); + + cy.get('button').contains('Agent policy 3').click(); + cy.getBySel('fleetAgentListTable').find('tr').should('have.length', 16); + + cy.getBySel('checkboxSelectAll').click(); + // Trigger a bulk upgrade + cy.getBySel('agentBulkActionsButton').click(); + cy.get('button').contains('Upgrade 15 agents').click(); + cy.get('.euiModalFooter button').contains('Upgrade 15 agents').click(); + // Cancel upgrade + cy.getBySel('abortUpgradeBtn').click(); + cy.get('button').contains('Confirm').click(); + }); + }); }); diff --git a/x-pack/plugins/fleet/cypress/screens/integrations.ts b/x-pack/plugins/fleet/cypress/screens/integrations.ts index dddede9e77f8d..ed645d08d9b5f 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/components/fleet_server_instructions/utils/install_command_utils.test.ts b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.test.ts index 12c1af65f9555..a2ef3b988cf19 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.test.ts @@ -17,8 +17,8 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip - tar xzvf elastic-agent--linux-x86_64.zip + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.tar.gz + tar xzvf elastic-agent--linux-x86_64.tar.gz cd elastic-agent--linux-x86_64 sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ @@ -51,8 +51,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz - Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz + "$ProgressPreference = 'SilentlyContinue' + wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.zip -OutFile elastic-agent--windows-x86_64.zip + Expand-Archive .\\\\elastic-agent--windows-x86_64.zip cd elastic-agent--windows-x86_64 .\\\\elastic-agent.exe install \` --fleet-server-es=http://elasticsearch:9200 \` @@ -69,11 +70,12 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm - tar xzvf elastic-agent--x86_64.rpm - cd elastic-agent--x86_64 + sudo rpm -vi elastic-agent--x86_64.rpm sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1" + --fleet-server-service-token=service-token-1 + sudo systemctl enable elastic-agent + sudo systemctl start elastic-agent" `); }); @@ -86,11 +88,12 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb - tar xzvf elastic-agent--amd64.deb - cd elastic-agent--amd64 + sudo dpkg -i elastic-agent--amd64.deb sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1" + --fleet-server-service-token=service-token-1 + sudo systemctl enable elastic-agent + sudo systemctl start elastic-agent" `); }); @@ -106,8 +109,8 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip - tar xzvf elastic-agent--linux-x86_64.zip + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.tar.gz + tar xzvf elastic-agent--linux-x86_64.tar.gz cd elastic-agent--linux-x86_64 sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ @@ -127,8 +130,8 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip - tar xzvf elastic-agent--linux-x86_64.zip + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.tar.gz + tar xzvf elastic-agent--linux-x86_64.tar.gz cd elastic-agent--linux-x86_64 sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ @@ -165,8 +168,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz - Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz + "$ProgressPreference = 'SilentlyContinue' + wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.zip -OutFile elastic-agent--windows-x86_64.zip + Expand-Archive .\\\\elastic-agent--windows-x86_64.zip cd elastic-agent--windows-x86_64 .\\\\elastic-agent.exe install \` --fleet-server-es=http://elasticsearch:9200 \` @@ -185,12 +189,13 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm - tar xzvf elastic-agent--x86_64.rpm - cd elastic-agent--x86_64 + sudo rpm -vi elastic-agent--x86_64.rpm sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ - --fleet-server-policy=policy-1" + --fleet-server-policy=policy-1 + sudo systemctl enable elastic-agent + sudo systemctl start elastic-agent" `); }); @@ -204,12 +209,13 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb - tar xzvf elastic-agent--amd64.deb - cd elastic-agent--amd64 + sudo dpkg -i elastic-agent--amd64.deb sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ - --fleet-server-policy=policy-1" + --fleet-server-policy=policy-1 + sudo systemctl enable elastic-agent + sudo systemctl start elastic-agent" `); }); }); @@ -226,8 +232,8 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip - tar xzvf elastic-agent--linux-x86_64.zip + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.tar.gz + tar xzvf elastic-agent--linux-x86_64.tar.gz cd elastic-agent--linux-x86_64 sudo ./elastic-agent install--url=http://fleetserver:8220 \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ @@ -276,8 +282,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz - Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz + "$ProgressPreference = 'SilentlyContinue' + wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.zip -OutFile elastic-agent--windows-x86_64.zip + Expand-Archive .\\\\elastic-agent--windows-x86_64.zip cd elastic-agent--windows-x86_64 .\\\\elastic-agent.exe install --url=http://fleetserver:8220 \` --fleet-server-es=http://elasticsearch:9200 \` @@ -302,8 +309,7 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm - tar xzvf elastic-agent--x86_64.rpm - cd elastic-agent--x86_64 + sudo rpm -vi elastic-agent--x86_64.rpm sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -311,7 +317,9 @@ describe('getInstallCommandForPlatform', () => { --certificate-authorities= \\\\ --fleet-server-es-ca= \\\\ --fleet-server-cert= \\\\ - --fleet-server-cert-key=" + --fleet-server-cert-key= + sudo systemctl enable elastic-agent + sudo systemctl start elastic-agent" `); }); @@ -327,8 +335,7 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb - tar xzvf elastic-agent--amd64.deb - cd elastic-agent--amd64 + sudo dpkg -i elastic-agent--amd64.deb sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -336,7 +343,9 @@ describe('getInstallCommandForPlatform', () => { --certificate-authorities= \\\\ --fleet-server-es-ca= \\\\ --fleet-server-cert= \\\\ - --fleet-server-cert-key=" + --fleet-server-cert-key= + sudo systemctl enable elastic-agent + sudo systemctl start elastic-agent" `); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts index ed38478c3a3ee..14cb60d76991f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts @@ -14,34 +14,40 @@ export type CommandsByPlatform = { function getArtifact(platform: PLATFORM_TYPE, kibanaVersion: string) { const ARTIFACT_BASE_URL = 'https://artifacts.elastic.co/downloads/beats/elastic-agent'; - const artifactMap: Record< - PLATFORM_TYPE, - { fullUrl: string; filename: string; unpackedDir: string } - > = { + const artifactMap: Record = { linux: { - fullUrl: `${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-linux-x86_64.zip`, - filename: `elastic-agent-${kibanaVersion}-linux-x86_64.zip`, - unpackedDir: `elastic-agent-${kibanaVersion}-linux-x86_64`, + downloadCommand: [ + `curl -L -O ${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-linux-x86_64.tar.gz`, + `tar xzvf elastic-agent-${kibanaVersion}-linux-x86_64.tar.gz`, + `cd elastic-agent-${kibanaVersion}-linux-x86_64`, + ].join(`\n`), }, mac: { - fullUrl: `${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-darwin-x86_64.tar.gz`, - filename: `elastic-agent-${kibanaVersion}-darwin-x86_64.tar.gz`, - unpackedDir: `elastic-agent-${kibanaVersion}-darwin-x86_64`, + downloadCommand: [ + `curl -L -O ${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-darwin-x86_64.tar.gz`, + `tar xzvf elastic-agent-${kibanaVersion}-darwin-x86_64.tar.gz`, + `cd elastic-agent-${kibanaVersion}-darwin-x86_64`, + ].join(`\n`), }, windows: { - fullUrl: `${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-windows-x86_64.tar.gz`, - filename: `elastic-agent-${kibanaVersion}-windows-x86_64.tar.gz`, - unpackedDir: `elastic-agent-${kibanaVersion}-windows-x86_64`, + downloadCommand: [ + `$ProgressPreference = 'SilentlyContinue'`, + `wget ${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip`, + `Expand-Archive .\\elastic-agent-${kibanaVersion}-windows-x86_64.zip`, + `cd elastic-agent-${kibanaVersion}-windows-x86_64`, + ].join(`\n`), }, deb: { - fullUrl: `${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-amd64.deb`, - filename: `elastic-agent-${kibanaVersion}-amd64.deb`, - unpackedDir: `elastic-agent-${kibanaVersion}-amd64`, + downloadCommand: [ + `curl -L -O ${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-amd64.deb`, + `sudo dpkg -i elastic-agent-${kibanaVersion}-amd64.deb`, + ].join(`\n`), }, rpm: { - fullUrl: `${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-x86_64.rpm`, - filename: `elastic-agent-${kibanaVersion}-x86_64.rpm`, - unpackedDir: `elastic-agent-${kibanaVersion}-x86_64`, + downloadCommand: [ + `curl -L -O ${ARTIFACT_BASE_URL}/elastic-agent-${kibanaVersion}-x86_64.rpm`, + `sudo rpm -vi elastic-agent-${kibanaVersion}-x86_64.rpm`, + ].join(`\n`), }, }; @@ -61,18 +67,6 @@ export function getInstallCommandForPlatform( const newLineSeparator = platform === 'windows' ? '`\n' : '\\\n'; const artifact = getArtifact(platform, kibanaVersion ?? ''); - const downloadCommand = - platform === 'windows' - ? [ - `wget ${artifact.fullUrl} -OutFile ${artifact.filename}`, - `Expand-Archive .\\${artifact.filename}`, - `cd ${artifact.unpackedDir}`, - ].join(`\n`) - : [ - `curl -L -O ${artifact.fullUrl}`, - `tar xzvf ${artifact.filename}`, - `cd ${artifact.unpackedDir}`, - ].join(`\n`); const commandArguments = []; @@ -108,11 +102,11 @@ export function getInstallCommandForPlatform( }, ''); const commands = { - linux: `${downloadCommand}\nsudo ./elastic-agent install${commandArgumentsStr}`, - mac: `${downloadCommand}\nsudo ./elastic-agent install ${commandArgumentsStr}`, - windows: `${downloadCommand}\n.\\elastic-agent.exe install ${commandArgumentsStr}`, - deb: `${downloadCommand}\nsudo elastic-agent enroll ${commandArgumentsStr}`, - rpm: `${downloadCommand}\nsudo elastic-agent enroll ${commandArgumentsStr}`, + linux: `${artifact.downloadCommand}\nsudo ./elastic-agent install${commandArgumentsStr}`, + mac: `${artifact.downloadCommand}\nsudo ./elastic-agent install ${commandArgumentsStr}`, + windows: `${artifact.downloadCommand}\n.\\elastic-agent.exe install ${commandArgumentsStr}`, + deb: `${artifact.downloadCommand}\nsudo elastic-agent enroll ${commandArgumentsStr}\nsudo systemctl enable elastic-agent\nsudo systemctl start elastic-agent`, + rpm: `${artifact.downloadCommand}\nsudo elastic-agent enroll ${commandArgumentsStr}\nsudo systemctl enable elastic-agent\nsudo systemctl start elastic-agent`, }; return commands[platform]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index 2845c545c2c98..c7948aff6c212 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; -import type { EuiBasicTableProps } from '@elastic/eui'; +import React, { memo, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -15,25 +14,40 @@ import { EuiTitle, EuiToolTip, EuiPanel, - EuiButtonIcon, - EuiBasicTable, + EuiSpacer, + EuiText, + EuiTreeView, + EuiBadge, + useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import styled from 'styled-components'; -import type { Agent, AgentPolicy, PackagePolicy, PackagePolicyInput } from '../../../../../types'; +import type { Agent, AgentPolicy, PackagePolicy } from '../../../../../types'; import { useLink, useUIExtension } from '../../../../../hooks'; import { ExtensionWrapper, PackageIcon } from '../../../../../components'; import { displayInputType, getLogsQueryByInputType } from './input_type_utils'; const StyledEuiAccordion = styled(EuiAccordion)` - .ingest-integration-title-button { - padding: ${(props) => props.theme.eui.paddingSizes.m}; + .euiAccordion__button { + width: 90%; + } + + .euiAccordion__triggerWrapper { + padding-left: ${(props) => props.theme.eui.paddingSizes.m}; + } + + &.euiAccordion-isOpen { + .euiAccordion__childWrapper { + padding: ${(props) => props.theme.eui.paddingSizes.m}; + padding-top: 0px; + } } - &.euiAccordion-isOpen .ingest-integration-title-button { - border-bottom: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; + .ingest-integration-title-button { + padding: ${(props) => props.theme.eui.paddingSizes.s}; } .euiTableRow:last-child .euiTableRowCell { @@ -43,6 +57,14 @@ const StyledEuiAccordion = styled(EuiAccordion)` .euiIEFlexWrapFix { min-width: 0; } + + .euiAccordion__buttonContent { + width: 100%; + } +`; + +const StyledEuiLink = styled(EuiLink)` + font-size: ${(props) => props.theme.eui.euiFontSizeS}; `; const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ @@ -54,7 +76,7 @@ const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ @@ -70,55 +92,75 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ packagePolicy: PackagePolicy; }> = memo(({ agent, agentPolicy, packagePolicy }) => { const { getHref } = useLink(); + const theme = useEuiTheme(); + const [showNeedsAttentionBadge, setShowNeedsAttentionBadge] = useState(false); const extensionView = useUIExtension( packagePolicy.package?.name ?? '', 'package-policy-response' ); - const inputs = useMemo(() => { - return packagePolicy.inputs.filter((input) => input.enabled); - }, [packagePolicy.inputs]); + const policyResponseExtensionView = useMemo(() => { + return ( + extensionView && ( + + + + ) + ); + }, [agent, extensionView]); - const columns: EuiBasicTableProps['columns'] = [ + const inputItems = [ { - field: 'type', - width: '100%', - name: i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeLabel', { - defaultMessage: 'Input', - }), - render: (inputType: string) => { - return displayInputType(inputType); - }, - }, - { - align: 'right', - name: i18n.translate('xpack.fleet.agentDetailsIntegrations.actionsLabel', { - defaultMessage: 'Actions', - }), - field: 'type', - width: 'auto', - render: (inputType: string) => { - return ( - - - - ); - }, + label: ( + + + + ), + id: 'inputs', + children: packagePolicy.inputs.reduce( + (acc: Array<{ label: JSX.Element; id: string }>, current) => { + if (current.enabled) { + return [ + ...acc, + { + label: ( + + + {displayInputType(current.type)} + + + ), + id: current.type, + }, + ]; + } + return acc; + }, + [] + ), }, ]; @@ -128,7 +170,7 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ title={

- + {packagePolicy.package ? ( + {showNeedsAttentionBadge && ( + + + + + + )}

} > - tableLayout="auto" items={inputs} columns={columns} /> - {extensionView && ( - - - - )} + + {policyResponseExtensionView} + ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx index 6c8bf0656b3a1..b4677761be1a2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx @@ -8,7 +8,7 @@ import React, { memo, useState, useEffect, useCallback } from 'react'; import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { DataView } from '@kbn/data-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { useStartServices } from '../../../../../hooks'; @@ -35,7 +35,7 @@ export const DatasetFilter: React.FunctionComponent<{ title: AGENT_LOG_INDEX_PATTERN, fields: [DATASET_FIELD], } as DataView, - field: DATASET_FIELD, + field: DATASET_FIELD as DataViewField, query: '', }); if (values.length > 0) setDatasetValues(values.sort()); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx index cade1277ddfbc..b512b0a5643d6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx @@ -8,7 +8,7 @@ import React, { memo, useState, useEffect, useCallback } from 'react'; import { EuiPopover, EuiFilterButton, EuiFilterSelectItem, EuiIcon, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { DataView } from '@kbn/data-plugin/common'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { useStartServices } from '../../../../../hooks'; @@ -46,7 +46,7 @@ export const LogLevelFilter: React.FunctionComponent<{ title: AGENT_LOG_INDEX_PATTERN, fields: [LOG_LEVEL_FIELD], } as DataView, - field: LOG_LEVEL_FIELD, + field: LOG_LEVEL_FIELD as DataViewField, query: '', }); setLevelValues(sortLogLevels(values)); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index 7df96dad06a88..01411d6c37614 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -62,6 +62,9 @@ export const AgentBulkActions: React.FunctionComponent = ({ selectionMode === 'manual' ? !!selectedAgents.find((agent) => agent.active) : totalAgents > totalInactiveAgents; + const totalActiveAgents = totalAgents - totalInactiveAgents; + const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents; + const agents = selectionMode === 'manual' ? selectedAgents : currentQuery; const panels = [ { @@ -87,7 +90,10 @@ export const AgentBulkActions: React.FunctionComponent = ({ ), icon: , @@ -102,7 +108,10 @@ export const AgentBulkActions: React.FunctionComponent = ({ ), icon: , @@ -117,7 +126,10 @@ export const AgentBulkActions: React.FunctionComponent = ({ ), icon: , @@ -131,10 +143,6 @@ export const AgentBulkActions: React.FunctionComponent = ({ }, ]; - const totalActiveAgents = totalAgents - totalInactiveAgents; - const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents; - const agents = selectionMode === 'manual' ? selectedAgents : currentQuery; - return ( <> {isReassignFlyoutOpen && ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/current_bulk_upgrade_callout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/current_bulk_upgrade_callout.tsx index 7f751dd36d0b1..0a29aace340a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/current_bulk_upgrade_callout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/current_bulk_upgrade_callout.tsx @@ -41,43 +41,47 @@ export const CurrentBulkUpgradeCallout: React.FunctionComponent { + if (!currentUpgrade.startTime) { + return false; + } const now = Date.now(); const startDate = new Date(currentUpgrade.startTime).getTime(); return startDate > now; }, [currentUpgrade]); - const calloutTitle = isScheduled ? ( - - -   - - - ), - }} - /> - ) : ( - - ); + const calloutTitle = + isScheduled && currentUpgrade.startTime ? ( + + +   + + + ), + }} + /> + ) : ( + + ); return ( - + setIsTagsFilterOpen(!isTagsFilterOpen)} isSelected={isTagsFilterOpen} hasActiveFilters={selectedTags.length > 0} - numFilters={selectedTags.length} + numActiveFilters={selectedTags.length} + numFilters={tags.length} disabled={tags.length === 0} data-test-subj="agentList.tagsFilter" > diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/constants.tsx index 7ef2a186829c8..15722017647e9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/constants.tsx @@ -23,4 +23,4 @@ export const FALLBACK_VERSIONS = [ '7.17.0', ]; -export const MAINTAINANCE_VALUES = [1, 2, 4, 8, 12, 24, 48]; +export const MAINTAINANCE_VALUES = [0, 1, 2, 4, 8, 12, 24, 48]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/hooks.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/hooks.test.tsx new file mode 100644 index 0000000000000..3f4fcea3ca5f7 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/hooks.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { act } from '@testing-library/react-hooks'; + +import { createFleetTestRendererMock } from '../../../../../../mock'; + +import { useScheduleDateTime } from './hooks'; + +jest.mock('../../../../../../hooks/use_fleet_status', () => ({ + FleetStatusProvider: (props: any) => { + return props.children; + }, + useFleetStatus: jest.fn().mockReturnValue({}), +})); + +describe('useScheduleDateTime', () => { + it('do not allow to set a date before the current time', async () => { + const renderer = createFleetTestRendererMock(); + const { result } = renderer.renderHook(() => useScheduleDateTime('2020-01-01T10:10:00.000Z')); + + act(() => result.current.onChangeStartDateTime(moment('2020-01-01T10:10:00.000Z'))); + + expect(result.current.startDatetime.toISOString()).toEqual('2020-01-01T10:10:00.000Z'); + }); + + it('allow to set a date after the current time', async () => { + const renderer = createFleetTestRendererMock(); + const { result } = renderer.renderHook(() => useScheduleDateTime('2020-01-01T10:10:00.000Z')); + + act(() => result.current.onChangeStartDateTime(moment('2020-01-01T10:15:00.000Z'))); + + expect(result.current.startDatetime.toISOString()).toEqual('2020-01-01T10:15:00.000Z'); + }); + + it('should set minTime and maxTime for the same day', async () => { + const renderer = createFleetTestRendererMock(); + const { result } = renderer.renderHook(() => useScheduleDateTime('2020-01-01')); + + expect(result.current.minTime).toBeDefined(); + expect(result.current.maxTime).toBeDefined(); + expect(result.current.minTime?.toISOString()).toEqual('2020-01-01T05:00:00.000Z'); + expect(result.current.maxTime?.toISOString()).toEqual('2020-01-02T04:59:59.999Z'); + }); + + it('should not set minTime and maxTime if the user choose a day in the future', async () => { + const renderer = createFleetTestRendererMock(); + const { result } = renderer.renderHook(() => useScheduleDateTime('2020-01-01')); + + act(() => result.current.onChangeStartDateTime(moment('2020-01-02'))); + + expect(result.current.minTime).not.toBeDefined(); + expect(result.current.maxTime).not.toBeDefined(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/hooks.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/hooks.tsx new file mode 100644 index 0000000000000..f26dd734354bd --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/hooks.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useState, useMemo, useCallback } from 'react'; +import moment from 'moment'; + +export function useScheduleDateTime(now?: string) { + const initialDatetime = useMemo(() => moment(now), [now]); + const [startDatetime, setStartDatetime] = useState(initialDatetime); + const minTime = useMemo(() => { + if (startDatetime.isSame(initialDatetime, 'day')) { + return initialDatetime.clone(); + } + }, [startDatetime, initialDatetime]); + const maxTime = useMemo(() => { + if (startDatetime.isSame(initialDatetime, 'day')) { + return initialDatetime.clone().endOf('day'); + } + }, [startDatetime, initialDatetime]); + + const onChangeStartDateTime = useCallback( + (date: moment.Moment | null) => { + if (!date) { + return; + } + + if (date.isBefore(initialDatetime)) { + setStartDatetime(initialDatetime); + } else { + setStartDatetime(date); + } + }, + [initialDatetime] + ); + + return { + startDatetime, + initialDatetime, + onChangeStartDateTime, + minTime, + maxTime, + }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx new file mode 100644 index 0000000000000..ed2689826dcd1 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { createFleetTestRendererMock } from '../../../../../../mock'; + +import { AgentUpgradeAgentModal } from '.'; +import type { AgentUpgradeAgentModalProps } from '.'; + +jest.mock('../../../../../../hooks/use_fleet_status', () => ({ + FleetStatusProvider: (props: any) => { + return props.children; + }, + useFleetStatus: jest.fn().mockReturnValue({}), +})); + +jest.mock('@elastic/eui', () => { + return { + ...jest.requireActual('@elastic/eui'), + EuiConfirmModal: ({ children }: any) => <>{children}, + }; +}); + +function renderAgentUpgradeAgentModal(props: Partial) { + const renderer = createFleetTestRendererMock(); + + const utils = renderer.render( + {}} {...props} /> + ); + + return { utils }; +} +describe('AgentUpgradeAgentModal', () => { + it('should set the default to Immediately if there is less than 10 agents using kuery', async () => { + const { utils } = renderAgentUpgradeAgentModal({ + agents: '*', + agentCount: 3, + }); + + const el = utils.container.querySelector( + '[data-test-subj="agentUpgradeModal.MaintainanceCombobox"]' + ); + expect(el).not.toBeNull(); + expect(el?.textContent).toBe('Immediately'); + }); + + it('should set the default to Immediately if there is less than 10 agents using selected agents', async () => { + const { utils } = renderAgentUpgradeAgentModal({ + agents: [{ id: 'agent1' }, { id: 'agent2' }] as any, + agentCount: 3, + }); + + const el = utils.container.querySelector( + '[data-test-subj="agentUpgradeModal.MaintainanceCombobox"]' + ); + expect(el).not.toBeNull(); + expect(el).not.toBeNull(); + expect(el?.textContent).toBe('Immediately'); + }); + + it('should set the default to 1 hour if there is more than 10 agents', async () => { + const { utils } = renderAgentUpgradeAgentModal({ + agents: '*', + agentCount: 13, + }); + + const el = utils.container.querySelector( + '[data-test-subj="agentUpgradeModal.MaintainanceCombobox"]' + ); + + expect(el).not.toBeNull(); + expect(el?.textContent).toBe('1 hour'); + }); +}); 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 5bd32f66b2811..a602c08e45102 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 @@ -7,7 +7,6 @@ import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import moment from 'moment'; import { EuiConfirmModal, EuiComboBox, @@ -26,6 +25,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'; @@ -37,8 +37,9 @@ import { } from '../../../../hooks'; import { FALLBACK_VERSIONS, MAINTAINANCE_VALUES } from './constants'; +import { useScheduleDateTime } from './hooks'; -interface Props { +export interface AgentUpgradeAgentModalProps { onClose: () => void; agents: Agent[] | string; agentCount: number; @@ -47,7 +48,7 @@ interface Props { const getVersion = (version: Array>) => version[0]?.value as string; -export const AgentUpgradeAgentModal: React.FunctionComponent = ({ +export const AgentUpgradeAgentModal: React.FunctionComponent = ({ onClose, agents, agentCount, @@ -59,17 +60,23 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ const [errors, setErrors] = useState(); const isSingleAgent = Array.isArray(agents) && agents.length === 1; - const isSmallBatch = Array.isArray(agents) && agents.length > 1 && agents.length <= 10; + const isSmallBatch = agentCount <= 10; const isAllAgents = agents === ''; - const fallbackVersions = [kibanaVersion].concat(FALLBACK_VERSIONS); + const fallbackVersions = useMemo( + () => [kibanaVersion].concat(FALLBACK_VERSIONS), + [kibanaVersion] + ); const minVersion = useMemo(() => { + if (!Array.isArray(agents)) { + return getMinVersion(fallbackVersions); + } const versions = (agents as Agent[]).map( (agent) => agent.local_metadata?.elastic?.agent?.version ); return getMinVersion(versions); - }, [agents]); + }, [agents, fallbackVersions]); const versionOptions: Array> = useMemo(() => { const displayVersions = minVersion @@ -81,9 +88,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ })); }, [fallbackVersions, minVersion]); - const maintainanceWindows = - isSmallBatch && !isScheduled ? [0].concat(MAINTAINANCE_VALUES) : MAINTAINANCE_VALUES; - const maintainanceOptions: Array> = maintainanceWindows.map( + const maintainanceOptions: Array> = MAINTAINANCE_VALUES.map( (option) => ({ label: option === 0 @@ -99,11 +104,11 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ ); const [selectedVersion, setSelectedVersion] = useState([versionOptions[0]]); const [selectedMantainanceWindow, setSelectedMantainanceWindow] = useState([ - maintainanceOptions[0], + isSmallBatch ? maintainanceOptions[0] : maintainanceOptions[1], ]); - const initialDatetime = useMemo(() => moment(), []); - const [startDatetime, setStartDatetime] = useState(initialDatetime); + const { startDatetime, onChangeStartDateTime, initialDatetime, minTime, maxTime } = + useScheduleDateTime(); async function onSubmit() { const version = getVersion(selectedVersion); @@ -195,6 +200,10 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ } const onCreateOption = (searchValue: string) => { + if (!semverValid(searchValue)) { + return; + } + const agentVersionNumber = semverCoerce(searchValue); if ( agentVersionNumber?.version && @@ -293,8 +302,12 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ fullWidth singleSelection={{ asPlainText: true }} options={versionOptions} + isClearable={false} selectedOptions={selectedVersion} onChange={(selected: Array>) => { + if (!selected.length) { + return; + } setSelectedVersion(selected); }} onCreateOption={onCreateOption} @@ -328,7 +341,9 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ showTimeSelect selected={startDatetime} minDate={initialDatetime} - onChange={(date) => setStartDatetime(date as moment.Moment)} + minTime={minTime} + maxTime={maxTime} + onChange={onChangeStartDateTime} />
@@ -365,10 +380,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 9c907436af6e3..46ddeb809980f 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 0000000000000..e2b4a3ba1fa1b --- /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/hooks/use_merge_epr_with_replacements.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts index 89163e0438a2d..412cdac83c4a7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts @@ -15,7 +15,7 @@ import { FLEET_APM_PACKAGE } from '../../../../common/constants'; function findReplacementsForEprPackage( replacements: CustomIntegration[], packageName: string, - release: 'beta' | 'experimental' | 'ga' + release?: 'beta' | 'experimental' | 'ga' ): CustomIntegration[] { if (release === 'ga') { return []; 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 0000000000000..0b1dbd6daef87 --- /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 a97a9ec8c1c24..53d6312a20773 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 07fd657a01708..b21c790edd0f3 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 9cb1599a3a8c6..45d11730adba9 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 798c5ce43e50b..cc11dd6819695 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/detail/utils/get_install_route_options.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts index 6f0c66150c8ce..82a3af7c81cc2 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts @@ -7,8 +7,20 @@ import type { CreatePackagePolicyRouteState } from '../../../../../types'; import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, pagePathGetters } from '../../../../../constants'; -// TODO: (in following PR) decide better way to infer this list of "special" integrations -const SPECIAL_PACKAGES = ['apm', 'endpoint', 'synthetics']; +// List of packages that shouldn't use the multi-step onboarding UI because they use custom policy interfaces +// or are otherwise not accounted for by verbiage and elements throughout the multi-step UI +const EXCLUDED_PACKAGES = [ + 'apm', + 'endpoint', + 'synthetics', + 'security_detection_engine', + 'osquery_manager', + 'dga', + 'cloud_security_posture', + 'problemchild', + 'kubernetes', +]; + interface GetInstallPkgRouteOptionsParams { currentPath: string; integration: string | null; @@ -20,7 +32,7 @@ interface GetInstallPkgRouteOptionsParams { } const isPackageExemptFromStepsLayout = (pkgkey: string) => - SPECIAL_PACKAGES.some((pkgname) => pkgkey.startsWith(pkgname)); + EXCLUDED_PACKAGES.some((pkgname) => pkgkey.startsWith(pkgname)); /* * When the install package button is pressed, this fn decides which page to navigate to * by generating the options to be passed to `services.application.navigateToApp`. 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 0898f099e3e8c..4322f434ddc70 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; @@ -70,7 +71,7 @@ export const mapToCard = ( let release: 'ga' | 'beta' | 'experimental' | undefined; if ('release' in item) { release = item.release; - } else if (item.isBeta === true) { + } else if ((item as CustomIntegration).isBeta === true) { release = 'beta'; } @@ -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 964994c250aa0..19de166cebc16 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/agent_enrollment_flyout/agent_policy_select_create.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx index 304239ac77dad..4dab3b054bc8d 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx @@ -14,7 +14,7 @@ import { } from '../../applications/fleet/sections/agents/components'; import { AgentPolicyCreateInlineForm } from '../../applications/fleet/sections/agent_policy/components'; import type { AgentPolicy } from '../../types'; -import { incrementPolicyName } from '../../services'; +import { incrementPolicyName, policyHasFleetServer } from '../../services'; import { AgentPolicySelection } from '.'; @@ -48,6 +48,18 @@ export const SelectCreateAgentPolicy: React.FC = ({ ); }, [agentPolicies, excludeFleetServer]); + useEffect(() => { + // Select default value if policy has no fleet server + if ( + regularAgentPolicies.length === 1 && + !selectedPolicyId && + excludeFleetServer !== false && + !policyHasFleetServer(regularAgentPolicies[0]) + ) { + setSelectedPolicyId(regularAgentPolicies[0].id); + } + }, [regularAgentPolicies, selectedPolicyId, setSelectedPolicyId, excludeFleetServer]); + const onAgentPolicyChange = useCallback( async (key?: string, policy?: AgentPolicy) => { if (policy) { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx index 24d8def12bbdc..97c0542ab2477 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/instructions.tsx @@ -70,6 +70,8 @@ export const Instructions = (props: InstructionProps) => { return settings?.fleet_server_hosts || []; }, [settings]); + if (isLoadingAgents || isLoadingAgentPolicies) return ; + const hasNoFleetServerHost = fleetStatus.isReady && fleetServerHosts.length === 0; const showAgentEnrollment = @@ -89,8 +91,6 @@ export const Instructions = (props: InstructionProps) => { setSelectionType('tabs'); } - if (isLoadingAgents || isLoadingAgentPolicies) return ; - if (hasNoFleetServerHost) { return 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 ca7293a8c99c9..74385a7eff0c8 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 2d9326cf6cbb1..e16c0e5dec867 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 a87c9fe9e0869..26db09d68f41d 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/mock/fleet_start_services.tsx b/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx index 86188d15b5191..c5d9b50111569 100644 --- a/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx +++ b/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n-react'; -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import { coreMock } from '@kbn/core/public/mocks'; import type { IStorage } from '@kbn/kibana-utils-plugin/public'; diff --git a/x-pack/plugins/fleet/public/mock/types.ts b/x-pack/plugins/fleet/public/mock/types.ts index 44d88acae1617..4f4387369a423 100644 --- a/x-pack/plugins/fleet/public/mock/types.ts +++ b/x-pack/plugins/fleet/public/mock/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import type { FleetSetupDeps, FleetStart, FleetStartDeps, FleetStartServices } from '../plugin'; 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 a36a72ee3516c..e3ffef62ac4ec 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/public/types/ui_extensions.ts b/x-pack/plugins/fleet/public/types/ui_extensions.ts index f4e90ee152dbe..17e9a25c656d0 100644 --- a/x-pack/plugins/fleet/public/types/ui_extensions.ts +++ b/x-pack/plugins/fleet/public/types/ui_extensions.ts @@ -8,7 +8,7 @@ import type { EuiStepProps } from '@elastic/eui'; import type { ComponentType, LazyExoticComponent } from 'react'; -import type { NewPackagePolicy, PackageInfo, PackagePolicy } from '.'; +import type { Agent, NewPackagePolicy, PackageInfo, PackagePolicy } from '.'; /** Register a Fleet UI extension */ export type UIExtensionRegistrationCallback = (extensionPoint: UIExtensionPoint) => void; @@ -54,8 +54,10 @@ export type PackagePolicyResponseExtensionComponent = ComponentType; export interface PackagePolicyResponseExtensionComponentProps { - /** The current host id to retrieve response from */ - endpointId: string; + /** The current agent to retrieve response from */ + agent: Agent; + /** A callback function to set the `needs attention` state */ + onShowNeedsAttentionBadge?: (val: boolean) => void; } /** Extension point registration contract for Integration Policy Edit views */ 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 4a93afd73b130..c82eb5333fc9f 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 { @@ -124,7 +125,15 @@ Object { ], "output_permissions": Object { "es-containerhost": Object { - "Elastic APM": Object { + "_elastic_agent_checks": Object { + "cluster": Array [ + "monitor", + ], + }, + "_elastic_agent_monitoring": Object { + "indices": Array [], + }, + "elastic-cloud-apm": Object { "cluster": Array [ "cluster:monitor/main", ], @@ -206,14 +215,6 @@ Object { }, ], }, - "_elastic_agent_checks": Object { - "cluster": Array [ - "monitor", - ], - }, - "_elastic_agent_monitoring": Object { - "indices": Array [], - }, }, }, "outputs": 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 0442a48e22def..04bc8d37485ad 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 622cc8d147273..f400becfa0085 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/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 7b6c2dce0ef04..492505f84751e 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -141,16 +141,18 @@ export const createAgentPolicyHandler: FleetRequestHandler< } }; -export const updateAgentPolicyHandler: RequestHandler< +export const updateAgentPolicyHandler: FleetRequestHandler< TypeOf, unknown, TypeOf > = async (context, request, response) => { const coreContext = await context.core; + const fleetContext = await context.fleet; const soClient = coreContext.savedObjects.client; const esClient = coreContext.elasticsearch.client.asInternalUser; const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); const { force, ...data } = request.body; + const spaceId = fleetContext.spaceId; try { const agentPolicy = await agentPolicyService.update( soClient, @@ -160,6 +162,7 @@ export const updateAgentPolicyHandler: RequestHandler< { force, user: user || undefined, + spaceId, } ); const body: UpdateAgentPolicyResponse = { item: agentPolicy }; diff --git a/x-pack/plugins/fleet/server/routes/data_streams/get_metadata_from_terms_enum.ts b/x-pack/plugins/fleet/server/routes/data_streams/get_data_streams_query_metadata.ts similarity index 97% rename from x-pack/plugins/fleet/server/routes/data_streams/get_metadata_from_terms_enum.ts rename to x-pack/plugins/fleet/server/routes/data_streams/get_data_streams_query_metadata.ts index 11a45a7aab885..e2edece439053 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/get_metadata_from_terms_enum.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/get_data_streams_query_metadata.ts @@ -6,7 +6,7 @@ */ import type { ElasticsearchClient } from '@kbn/core/server'; -export async function getMetadataFromTermsEnum({ +export async function getDataStreamsQueryMetadata({ dataStreamName, esClient, }: { diff --git a/x-pack/plugins/fleet/server/routes/data_streams/get_metadata_from_aggregations.ts b/x-pack/plugins/fleet/server/routes/data_streams/get_metadata_from_aggregations.ts deleted file mode 100644 index 0673eeddde789..0000000000000 --- a/x-pack/plugins/fleet/server/routes/data_streams/get_metadata_from_aggregations.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { ElasticsearchClient } from '@kbn/core/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { map } from 'lodash'; - -export async function getMetadataFromAggregations({ - dataStreamName, - esClient, -}: { - dataStreamName: string; - esClient: ElasticsearchClient; -}) { - // Query backing indices to extract data stream dataset, namespace, and type values - const { aggregations: dataStreamAggs } = await esClient.search({ - index: dataStreamName, - body: { - size: 0, - query: { - bool: { - filter: [ - { - exists: { - field: 'data_stream.namespace', - }, - }, - { - exists: { - field: 'data_stream.dataset', - }, - }, - ], - }, - }, - aggs: { - maxIngestedTimestamp: { - max: { - field: 'event.ingested', - }, - }, - dataset: { - terms: { - field: 'data_stream.dataset', - size: 1, - }, - }, - namespace: { - terms: { - field: 'data_stream.namespace', - size: 1, - }, - }, - type: { - terms: { - field: 'data_stream.type', - size: 1, - }, - }, - serviceName: { - terms: { - field: 'service.name', - size: 2, - }, - }, - environment: { - terms: { - field: 'service.environment', - size: 2, - missing: 'ENVIRONMENT_NOT_DEFINED', - }, - }, - }, - }, - }); - - const { maxIngestedTimestamp } = dataStreamAggs as Record< - string, - estypes.AggregationsRateAggregate - >; - const { dataset, namespace, type, serviceName, environment } = dataStreamAggs as Record< - string, - estypes.AggregationsMultiBucketAggregateBase<{ key?: string; value?: number }> - >; - - const maxIngested = maxIngestedTimestamp?.value; - - return { - maxIngested, - dataset: (dataset.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', - namespace: (namespace.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', - type: (type.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', - serviceNames: map(serviceName.buckets, 'key') as string[], - environments: map(environment.buckets, 'key') as string[], - }; -} diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index 359ff3397ed5b..f68c8d287472e 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -7,17 +7,13 @@ import { keyBy, keys, merge } from 'lodash'; import type { RequestHandler } from '@kbn/core/server'; -import type { TypeOf } from '@kbn/config-schema'; - import type { DataStream } from '../../types'; import { KibanaSavedObjectType } from '../../../common'; import type { GetDataStreamsResponse } from '../../../common'; import { getPackageSavedObjects } from '../../services/epm/packages/get'; import { defaultIngestErrorHandler } from '../../errors'; -import type { GetDataStreamsListRequestSchema } from '../../../common/constants/data_streams'; -import { getMetadataFromTermsEnum } from './get_metadata_from_terms_enum'; -import { getMetadataFromAggregations } from './get_metadata_from_aggregations'; +import { getDataStreamsQueryMetadata } from './get_data_streams_query_metadata'; const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*,traces-*-*,synthetics-*-*'; @@ -47,10 +43,6 @@ export const getListHandler: RequestHandler = async (context, request, response) const { savedObjects, elasticsearch } = await context.core; const esClient = elasticsearch.client.asCurrentUser; - const { use_terms_enum: useTermsEnum } = request.params as TypeOf< - typeof GetDataStreamsListRequestSchema['params'] - >; - const body: GetDataStreamsResponse = { data_streams: [], }; @@ -137,9 +129,8 @@ export const getListHandler: RequestHandler = async (context, request, response) serviceDetails: null, }; - const { maxIngested, namespace, dataset, type, serviceNames, environments } = useTermsEnum - ? await getMetadataFromTermsEnum({ dataStreamName: dataStream.name, esClient }) - : await getMetadataFromAggregations({ dataStreamName: dataStream.name, esClient }); + const { maxIngested, namespace, dataset, type, serviceNames, environments } = + await getDataStreamsQueryMetadata({ dataStreamName: dataStream.name, esClient }); // some integrations e.g custom logs don't have event.ingested if (maxIngested) { diff --git a/x-pack/plugins/fleet/server/routes/data_streams/index.ts b/x-pack/plugins/fleet/server/routes/data_streams/index.ts index d7491d87e2a17..ddefc537ba207 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/index.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { GetDataStreamsListRequestSchema } from '../../../common/constants/data_streams'; import { DATA_STREAM_API_ROUTES } from '../../constants'; import type { FleetAuthzRouter } from '../security'; @@ -16,7 +15,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.get( { path: DATA_STREAM_API_ROUTES.LIST_PATTERN, - validate: GetDataStreamsListRequestSchema, + validate: false, fleetAuthz: { fleet: { all: true }, }, diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index a4b618d0323a1..6a9577b083f98 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -17,10 +17,6 @@ import type { } from '../../types'; import { agentPolicyService } from '../agent_policy'; import { outputService } from '../output'; -import { - storedPackagePoliciesToAgentPermissions, - DEFAULT_CLUSTER_PERMISSIONS, -} from '../package_policies_to_agent_permissions'; import { dataTypes, outputType } from '../../../common'; import type { FullAgentPolicyOutputPermissions } from '../../../common'; import { getSettings } from '../settings'; @@ -28,6 +24,10 @@ import { DEFAULT_OUTPUT } from '../../constants'; import { getMonitoringPermissions } from './monitoring_permissions'; import { storedPackagePoliciesToAgentInputs } from '.'; +import { + storedPackagePoliciesToAgentPermissions, + DEFAULT_CLUSTER_PERMISSIONS, +} from './package_policies_to_agent_permissions'; export async function getFullAgentPolicy( soClient: SavedObjectsClientContract, diff --git a/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts index 11e6a486d69c3..564c4fa351e14 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/monitoring_permissions.ts @@ -8,7 +8,6 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import { getPackageInfo, getInstallation } from '../epm/packages'; -import { getDataStreamPrivileges } from '../package_policies_to_agent_permissions'; import { PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, AGENT_POLICY_DEFAULT_MONITORING_DATASETS, @@ -17,6 +16,8 @@ import type { FullAgentPolicyOutputPermissions } from '../../../common'; import { FLEET_ELASTIC_AGENT_PACKAGE } from '../../../common'; import { dataTypes } from '../../../common'; +import { getDataStreamPrivileges } from './package_policies_to_agent_permissions'; + function buildDefault(enabled: { logs: boolean; metrics: boolean }, namespace: string) { let names: string[] = []; if (enabled.logs) { diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts similarity index 96% rename from x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts rename to x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts index 5c63d0ba5dca1..0db543e4357a4 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts @@ -5,14 +5,14 @@ * 2.0. */ -jest.mock('./epm/packages'); +jest.mock('../epm/packages'); import type { SavedObjectsClientContract } from '@kbn/core/server'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; -import type { PackagePolicy, RegistryDataStream } from '../types'; +import type { PackagePolicy, RegistryDataStream } from '../../types'; +import { getPackageInfo } from '../epm/packages'; -import { getPackageInfo } from './epm/packages'; import { getDataStreamPrivileges, storedPackagePoliciesToAgentPermissions, @@ -108,7 +108,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { const packagePolicies: PackagePolicy[] = [ { - id: '12345', + id: 'package-policy-uuid-test-123', name: 'test-policy', namespace: 'test', enabled: true, @@ -149,7 +149,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); expect(permissions).toMatchObject({ - 'test-policy': { + 'package-policy-uuid-test-123': { indices: [ { names: ['logs-some-logs-test'], @@ -213,7 +213,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { const packagePolicies: PackagePolicy[] = [ { - id: '12345', + id: 'package-policy-uuid-test-123', name: 'test-policy', namespace: 'test', enabled: true, @@ -244,7 +244,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); expect(permissions).toMatchObject({ - 'test-policy': { + 'package-policy-uuid-test-123': { indices: [ { names: ['logs-compiled-test'], @@ -308,7 +308,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { const packagePolicies: PackagePolicy[] = [ { - id: '12345', + id: 'package-policy-uuid-test-123', name: 'test-policy', namespace: 'test', enabled: true, @@ -344,7 +344,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); expect(permissions).toMatchObject({ - 'test-policy': { + 'package-policy-uuid-test-123': { indices: [ { names: ['logs-compiled-test'], @@ -422,7 +422,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { const packagePolicies: PackagePolicy[] = [ { - id: '12345', + id: 'package-policy-uuid-test-123', name: 'test-policy', namespace: 'test', enabled: true, @@ -453,7 +453,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); expect(permissions).toMatchObject({ - 'test-policy': { + 'package-policy-uuid-test-123': { indices: [ { names: ['logs-osquery_manager.result-test'], diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts similarity index 94% rename from x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts rename to x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts index 968282e2e587a..1fa8a35368cb5 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts @@ -6,12 +6,15 @@ */ import type { SavedObjectsClientContract } from '@kbn/core/server'; -import type { FullAgentPolicyOutputPermissions, RegistryDataStreamPrivileges } from '../../common'; -import { PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES } from '../constants'; +import type { + FullAgentPolicyOutputPermissions, + RegistryDataStreamPrivileges, +} from '../../../common'; +import { PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES } from '../../constants'; -import type { PackagePolicy } from '../types'; +import type { PackagePolicy } from '../../types'; -import { getPackageInfo } from './epm/packages'; +import { getPackageInfo } from '../epm/packages'; export const DEFAULT_CLUSTER_PERMISSIONS = ['monitor']; @@ -120,7 +123,7 @@ export async function storedPackagePoliciesToAgentPermissions( } return [ - packagePolicy.name, + packagePolicy.id, { indices: dataStreamsForPermissions.map((ds) => getDataStreamPrivileges(ds, packagePolicy.namespace) diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index d3ce069ed5cc6..1e86354513e26 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -18,6 +18,8 @@ import type { import type { AuthenticatedUser } from '@kbn/security-plugin/server'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; + import { AGENT_POLICY_SAVED_OBJECT_TYPE, AGENTS_PREFIX, @@ -40,6 +42,7 @@ import { AGENT_POLICY_INDEX, UUID_V5_NAMESPACE, FLEET_APM_PACKAGE, + FLEET_ELASTIC_AGENT_PACKAGE, } from '../../common'; import type { DeleteAgentPolicyResponse, @@ -59,7 +62,7 @@ import { elasticAgentManagedManifest, } from './elastic_agent_manifest'; -import { getPackageInfo } from './epm/packages'; +import { getPackageInfo, bulkInstallPackages } from './epm/packages'; import { getAgentsByKuery } from './agents'; import { packagePolicyService } from './package_policy'; import { incrementPackagePolicyCopyName } from './package_policies'; @@ -350,7 +353,7 @@ class AgentPolicyService { esClient: ElasticsearchClient, id: string, agentPolicy: Partial, - options?: { user?: AuthenticatedUser; force?: boolean } + options?: { user?: AuthenticatedUser; force?: boolean; spaceId?: string } ): Promise { if (agentPolicy.name) { await this.requireUniqueName(soClient, { @@ -374,6 +377,19 @@ class AgentPolicyService { } }); } + const { monitoring_enabled: monitoringEnabled } = agentPolicy; + const packagesToInstall = []; + if (!existingAgentPolicy.monitoring_enabled && monitoringEnabled?.length) { + packagesToInstall.push(FLEET_ELASTIC_AGENT_PACKAGE); + } + if (packagesToInstall.length > 0) { + await bulkInstallPackages({ + savedObjectsClient: soClient, + esClient, + packagesToInstall, + spaceId: options?.spaceId || DEFAULT_SPACE_ID, + }); + } return this._update(soClient, esClient, id, agentPolicy, options?.user); } diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 78c208eb5ae8e..2c839bdd7bb09 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -315,11 +315,6 @@ async function _getUpgradeActions(esClient: ElasticsearchClient, now = new Date( field: 'agents', }, }, - { - exists: { - field: 'start_time', - }, - }, { range: { expiration: { gte: now }, @@ -343,7 +338,7 @@ async function _getUpgradeActions(esClient: ElasticsearchClient, now = new Date( complete: false, nbAgentsAck: 0, version: hit._source.data?.version as string, - startTime: hit._source.start_time as string, + startTime: hit._source?.start_time, }; } diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts index 74cf93c1c6bcb..9921a2d97cdd9 100644 --- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -178,6 +178,8 @@ rules: - services - configmaps - serviceaccounts + - persistentvolumes + - persistentvolumeclaims verbs: ["get", "list", "watch"] # Enable this rule only if planing to use kubernetes_secrets provider #- apiGroups: [""] @@ -193,6 +195,7 @@ rules: - statefulsets - deployments - replicasets + - daemonsets verbs: ["get", "list", "watch"] - apiGroups: ["batch"] resources: @@ -441,6 +444,8 @@ rules: - services - configmaps - serviceaccounts + - persistentvolumes + - persistentvolumeclaims verbs: ["get", "list", "watch"] # Enable this rule only if planing to use kubernetes_secrets provider #- apiGroups: [""] @@ -456,6 +461,7 @@ rules: - statefulsets - deployments - replicasets + - daemonsets verbs: ["get", "list", "watch"] - apiGroups: - "" diff --git a/x-pack/plugins/fleet/server/services/epm/archive/parse.ts b/x-pack/plugins/fleet/server/services/epm/archive/parse.ts index 14b1eb8c305b8..30cd13137ff59 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/parse.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/parse.ts @@ -8,6 +8,8 @@ import { merge } from '@kbn/std'; import yaml from 'js-yaml'; import { pick, uniq } from 'lodash'; +import semverMajor from 'semver/functions/major'; +import semverPrerelease from 'semver/functions/prerelease'; import type { ArchivePackage, @@ -93,7 +95,6 @@ const requiredArchivePackageProps: readonly RequiredPackageProp[] = [ 'description', 'title', 'format_version', - 'release', 'owner', ] as const; @@ -108,6 +109,7 @@ const optionalArchivePackageProps: readonly OptionalPackageProp[] = [ 'screenshots', 'icons', 'policy_templates', + 'release', ] as const; const registryInputProps = Object.values(RegistryInputKeys); @@ -206,6 +208,14 @@ function parseAndVerifyArchive(paths: string[]): ArchivePackage { parsed.readme = readme; } + // If no `release` is specified, fall back to a value based on the `version` of the integration + // to maintain backwards comptability. This is a temporary measure until the `release` field is + // completely deprecated elsewhere in Fleet/Agent. See https://github.com/elastic/package-spec/issues/225 + if (!parsed.release) { + parsed.release = + semverPrerelease(parsed.version) || semverMajor(parsed.version) < 1 ? 'beta' : 'ga'; + } + return parsed; } 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 ab563255ea164..93c67a11e2d0e 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/graph/public/helpers/use_workspace_loader.test.tsx b/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx index 0c05fce5a4558..bfb373712acd6 100644 --- a/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx +++ b/x-pack/plugins/graph/public/helpers/use_workspace_loader.test.tsx @@ -9,7 +9,7 @@ import { coreMock } from '@kbn/core/public/mocks'; import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; import { createMockGraphStore } from '../state_management/mocks'; import { Workspace } from '../types'; -import { SavedObjectsClientCommon } from '@kbn/data-plugin/common'; +import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/public'; import { renderHook, act, RenderHookOptions } from '@testing-library/react-hooks'; jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts b/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts index 80c0f324e793f..a8b78bf2efccf 100644 --- a/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts +++ b/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts @@ -84,6 +84,6 @@ export const createIndexPatternsStartMock = ( indexPatterns: IndexPatternMock[] ): any => { return { - indexPatternsServiceFactory: async () => createIndexPatternsMock(asyncDelay, indexPatterns), + dataViewsServiceFactory: async () => createIndexPatternsMock(asyncDelay, indexPatterns), }; }; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 7aa4b433c477c..0872283e6af9e 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -219,7 +219,7 @@ export class KibanaFramework { elasticsearchClient: ElasticsearchClient ) { const [, startPlugins] = await this.core.getStartServices(); - return startPlugins.data.indexPatterns.indexPatternsServiceFactory( + return startPlugins.data.indexPatterns.dataViewsServiceFactory( savedObjectsClient, elasticsearchClient ); 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 782c65d8a0beb..82c4afb49ed3e 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 843a1a79eaf62..b4285863dbccb 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/common/expressions/format_column/format_column_fn.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts index bca56b2dbddbe..ff6ea3f443dcc 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts @@ -67,7 +67,7 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( }; // Some parent formatters are multi-fields and wrap the custom format into a "paramsPerField" // property. Here the format is passed to this property to make it work properly - if (col.meta.params?.params?.paramsPerField?.length) { + if ((col.meta.params?.params?.paramsPerField as SerializedFieldFormat[])?.length) { return withParams(col, { id: parentFormatId, params: { @@ -77,13 +77,13 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( // some wrapper formatters require params to be flatten out (i.e. terms) while others // require them to be in the params property (i.e. ranges) // so for now duplicate - paramsPerField: col.meta.params?.params?.paramsPerField.map( - (f: { id: string | undefined; params: Record | undefined }) => ({ - ...f, - params: { ...f.params, ...customParams }, - ...customParams, - }) - ), + paramsPerField: ( + col.meta.params?.params?.paramsPerField as SerializedFieldFormat[] + ).map((f) => ({ + ...f, + params: { ...f.params, ...customParams }, + ...customParams, + })), }, }); } 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 4ae1b8860c878..c17417e5106a1 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 367349f17a5b2..4ed822e7dc2f6 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 e673108585524..a3900d229363f 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/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index bcf6f50d2bd46..0bdcb22c280b2 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -11,11 +11,7 @@ import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { EuiDataGrid } from '@elastic/eui'; import { IAggType } from '@kbn/data-plugin/public'; -import { - FieldFormatParams, - IFieldFormat, - SerializedFieldFormat, -} from '@kbn/field-formats-plugin/common'; +import { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import { VisualizationContainer } from '../../visualization_container'; import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; @@ -110,7 +106,7 @@ describe('DatatableComponent', () => { x as IFieldFormat} + formatFactory={(x) => x as unknown as IFieldFormat} dispatchEvent={onDispatchEvent} getType={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} @@ -130,7 +126,7 @@ describe('DatatableComponent', () => { x as IFieldFormat} + formatFactory={(x) => x as unknown as IFieldFormat} dispatchEvent={onDispatchEvent} getType={jest.fn()} rowHasRowClickTriggerActions={[true, true, true]} @@ -151,7 +147,7 @@ describe('DatatableComponent', () => { x as IFieldFormat} + formatFactory={(x) => x as unknown as IFieldFormat} dispatchEvent={onDispatchEvent} getType={jest.fn()} rowHasRowClickTriggerActions={[false, false, false]} @@ -729,7 +725,7 @@ describe('DatatableComponent', () => { x as IFieldFormat} + formatFactory={(x) => x as unknown as IFieldFormat} dispatchEvent={onDispatchEvent} getType={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} @@ -764,7 +760,7 @@ describe('DatatableComponent', () => { x as IFieldFormat} + formatFactory={(x) => x as unknown as IFieldFormat} dispatchEvent={onDispatchEvent} getType={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} @@ -789,7 +785,7 @@ describe('DatatableComponent', () => { const defaultProps = { data, - formatFactory: (x?: SerializedFieldFormat) => x as IFieldFormat, + formatFactory: (x?: SerializedFieldFormat) => x as unknown as IFieldFormat, dispatchEvent: onDispatchEvent, getType: jest.fn(), paletteService: chartPluginMock.createPaletteRegistry(), @@ -825,7 +821,7 @@ describe('DatatableComponent', () => { x as IFieldFormat} + formatFactory={(x) => x as unknown as IFieldFormat} dispatchEvent={onDispatchEvent} getType={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index bc7770e815ba6..fff323ae4293b 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/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 790873fdc74b2..dc0401833acb6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -96,6 +96,7 @@ export function DimensionEditor(props: DimensionEditorProps) { http: props.http, storage: props.storage, unifiedSearch: props.unifiedSearch, + dataViews: props.dataViews, }; const { fieldByOperation, operationWithoutField } = operationSupportMatrix; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 25ad7dfb97b4c..7fc76300a73ec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -17,6 +17,7 @@ import { EuiButtonIcon, } from '@elastic/eui'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { IndexPatternDimensionEditorComponent, @@ -210,6 +211,7 @@ describe('IndexPatternDimensionEditorPanel', () => { savedObjectsClient: {} as SavedObjectsClientContract, http: {} as HttpSetup, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), data: { fieldFormats: { getType: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 4f20db3004e8b..f5961b89e4bff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -10,6 +10,7 @@ import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/c import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types'; import { GenericIndexPatternColumn } from '../indexpattern'; import { isColumnInvalid } from '../utils'; @@ -33,6 +34,7 @@ export type IndexPatternDimensionEditorProps = http: HttpSetup; data: DataPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; + dataViews: DataViewsPublicPluginStart; uniqueLabel: string; dateRange: DateRange; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index 412f8211844b2..66714f494bf53 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -7,6 +7,7 @@ import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { IndexPatternDimensionEditorProps } from '../dimension_panel'; import { onDrop } from './on_drop_handler'; import { getDropProps } from './get_drop_props'; @@ -324,6 +325,7 @@ describe('IndexPatternDimensionEditorPanel', () => { } as unknown as DataPublicPluginStart['fieldFormats'], } as unknown as DataPublicPluginStart, unifiedSearch: {} as UnifiedSearchPublicPluginStart, + dataViews: {} as DataViewsPublicPluginStart, core: {} as CoreSetup, dimensionGroups: [], isFullscreen: false, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index 804cbde3d170f..857d0cfb9c1d2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -11,6 +11,7 @@ import { act } from 'react-dom/test-utils'; import { EuiComboBox } from '@elastic/eui'; import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -56,6 +57,7 @@ describe('reference editor', () => { http: {} as HttpSetup, data: {} as DataPublicPluginStart, unifiedSearch: {} as UnifiedSearchPublicPluginStart, + dataViews: dataViewPluginMocks.createStartContract(), dimensionGroups: [], isFullscreen: false, toggleFullscreen: jest.fn(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index 3c16d271401ad..4082580cb456a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DateRange } from '../../../common'; @@ -67,6 +68,7 @@ export interface ReferenceEditorProps { http: HttpSetup; data: DataPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; + dataViews: DataViewsPublicPluginStart; paramEditorCustomProps?: ParamEditorCustomProps; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 8ed569ddfd328..a92d0883cc929 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'; @@ -380,6 +381,7 @@ export function getIndexPatternDatasource({ http={core.http} data={data} unifiedSearch={unifiedSearch} + dataViews={dataViews} uniqueLabel={columnLabelMap[props.columnId]} {...props} /> @@ -532,8 +534,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/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index e1e5f39a8cc48..d801387c30b29 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -11,6 +11,7 @@ import { dateHistogramOperation } from '.'; import { mount, shallow } from 'enzyme'; import { EuiSwitch } from '@elastic/eui'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/public'; @@ -22,6 +23,7 @@ import { act } from 'react-dom/test-utils'; const dataStart = dataPluginMock.createStartContract(); const unifiedSearchStart = unifiedSearchPluginMock.createStartContract(); +const dataViewsStart = dataViewPluginMocks.createStartContract(); dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression( (path: string) => { if (path === UI_SETTINGS.HISTOGRAM_MAX_BARS) { @@ -97,6 +99,7 @@ const defaultOptions = { }, data: dataStart, unifiedSearch: unifiedSearchStart, + dataViews: dataViewsStart, http: {} as HttpSetup, indexPattern: indexPattern1, operationDefinitionMap: {}, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index 7208965ec080c..1bfa10be4107b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -12,6 +12,7 @@ import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { FiltersIndexPatternColumn } from '.'; import { filtersOperation } from '..'; import type { IndexPatternLayer } from '../../../types'; @@ -27,6 +28,7 @@ const defaultProps = { dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), operationDefinitionMap: {}, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx index ecc46babcfe71..c8f74c0936fb9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx @@ -90,6 +90,7 @@ export function FormulaEditor({ indexPattern, operationDefinitionMap, unifiedSearch, + dataViews, toggleFullscreen, isFullscreen, setIsCloseable, @@ -417,6 +418,7 @@ export function FormulaEditor({ indexPattern, operationDefinitionMap: visibleOperationsMap, unifiedSearch, + dataViews, dateHistogramInterval: baseIntervalRef.current, }); } @@ -428,6 +430,7 @@ export function FormulaEditor({ indexPattern, operationDefinitionMap: visibleOperationsMap, unifiedSearch, + dataViews, dateHistogramInterval: baseIntervalRef.current, }); } @@ -444,7 +447,7 @@ export function FormulaEditor({ ), }; }, - [indexPattern, visibleOperationsMap, unifiedSearch, baseIntervalRef] + [indexPattern, visibleOperationsMap, unifiedSearch, dataViews, baseIntervalRef] ); const provideSignatureHelp = useCallback( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts index 0039f486933b9..8a59636d09952 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts @@ -8,6 +8,7 @@ import { parse } from '@kbn/tinymath'; import { monaco } from '@kbn/monaco'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../../../mocks'; import { GenericOperationDefinition } from '../..'; import type { IndexPatternField } from '../../../../types'; @@ -218,6 +219,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { @@ -239,6 +241,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toHaveLength(2); ['sum', 'last_value'].forEach((key) => { @@ -257,6 +260,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toEqual(['window']); }); @@ -272,6 +276,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toEqual([]); }); @@ -287,6 +292,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { @@ -308,6 +314,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { @@ -329,6 +336,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toHaveLength(0); }); @@ -344,6 +352,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toEqual(['bytes', 'memory']); }); @@ -359,6 +368,7 @@ describe('math completion', () => { indexPattern: createMockedIndexPattern(), operationDefinitionMap, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }); expect(results.list).toEqual(['bytes', 'memory']); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts index d793ace3b5196..95833a4508fdf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts @@ -20,8 +20,8 @@ import type { UnifiedSearchPublicPluginStart, QuerySuggestion, } from '@kbn/unified-search-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { parseTimeShift } from '@kbn/data-plugin/common'; -import type { DataView } from '@kbn/data-views-plugin/public'; import { IndexPattern } from '../../../../types'; import { memoizedGetAvailableOperationsByMetadata } from '../../../operations'; import { tinymathFunctions, groupArgsByType, unquotedStringRegex } from '../util'; @@ -121,6 +121,7 @@ export async function suggest({ context, indexPattern, operationDefinitionMap, + dataViews, unifiedSearch, dateHistogramInterval, }: { @@ -130,6 +131,7 @@ export async function suggest({ indexPattern: IndexPattern; operationDefinitionMap: Record; unifiedSearch: UnifiedSearchPublicPluginStart; + dataViews: DataViewsPublicPluginStart; dateHistogramInterval?: number; }): Promise { const text = @@ -150,6 +152,7 @@ export async function suggest({ return await getNamedArgumentSuggestions({ ast: tokenAst as TinymathNamedArgument, unifiedSearch, + dataViews, indexPattern, dateHistogramInterval, }); @@ -333,12 +336,14 @@ function getArgumentSuggestions( export async function getNamedArgumentSuggestions({ ast, unifiedSearch, + dataViews, indexPattern, dateHistogramInterval, }: { ast: TinymathNamedArgument; indexPattern: IndexPattern; unifiedSearch: UnifiedSearchPublicPluginStart; + dataViews: DataViewsPublicPluginStart; dateHistogramInterval?: number; }) { if (ast.name === 'shift') { @@ -372,7 +377,7 @@ export async function getNamedArgumentSuggestions({ query, selectionStart: position, selectionEnd: position, - indexPatterns: [indexPattern as unknown as DataView], + indexPatterns: [await dataViews.get(indexPattern.id)], boolFilter: [], }); return { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index efddd9d533f62..6ca79009ff95b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -15,6 +15,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { ExpressionAstFunction } from '@kbn/expressions-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { termsOperation } from './terms'; import { filtersOperation } from './filters'; import { cardinalityOperation } from './cardinality'; @@ -169,6 +170,7 @@ export interface ParamEditorProps { dateRange: DateRange; data: DataPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; + dataViews: DataViewsPublicPluginStart; activeData?: IndexPatternDimensionEditorProps['activeData']; operationDefinitionMap: Record; paramEditorCustomProps?: ParamEditorCustomProps; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index 5e3f1f0043664..242bdeaa677cb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -12,6 +12,7 @@ import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { LastValueIndexPatternColumn } from './last_value'; import { lastValueOperation } from '.'; @@ -27,6 +28,7 @@ const defaultProps = { savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), data: dataPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index 831bb03c89abd..ae8ba7d965ea7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -14,6 +14,7 @@ import { shallow, mount } from 'enzyme'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { percentileOperation } from '.'; import { IndexPattern, IndexPatternLayer } from '../../types'; @@ -38,6 +39,7 @@ const defaultProps = { dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), http: {} as HttpSetup, indexPattern: { ...createMockedIndexPattern(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.test.tsx index 4812c782a5f67..a7dbeedee633b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.test.tsx @@ -14,6 +14,7 @@ import { shallow, mount } from 'enzyme'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { percentileRanksOperation } from '.'; import { IndexPattern, IndexPatternLayer } from '../../types'; @@ -38,6 +39,7 @@ const defaultProps = { dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), http: {} as HttpSetup, indexPattern: { ...createMockedIndexPattern(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index c48584723eab9..30f883083072b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -12,6 +12,7 @@ import { EuiFieldNumber, EuiRange, EuiButtonEmpty, EuiLink, EuiText } from '@ela import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IndexPatternLayer, IndexPattern } from '../../../types'; import { rangeOperation } from '..'; @@ -53,6 +54,7 @@ jest.mock('lodash', () => { const dataPluginMockValue = dataPluginMock.createStartContract(); const unifiedSearchPluginMockValue = unifiedSearchPluginMock.createStartContract(); +const dataViewsPluginMockValue = dataViewPluginMocks.createStartContract(); // need to overwrite the formatter field first dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ id, params }) => { return { @@ -87,6 +89,7 @@ const defaultOptions = { }, data: dataPluginMockValue, unifiedSearch: unifiedSearchPluginMockValue, + dataViews: dataViewsPluginMockValue, http: {} as HttpSetup, indexPattern: { id: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx index 60a871efd85cf..df96b02ba2c95 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -13,6 +13,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { shallow, mount } from 'enzyme'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { staticValueOperation } from '.'; import { IndexPattern, IndexPatternLayer } from '../../types'; @@ -37,6 +38,7 @@ const defaultProps = { dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), http: {} as HttpSetup, indexPattern: { ...createMockedIndexPattern(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index fa3229223907a..1cce6c5b06cd6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -18,6 +18,7 @@ import type { import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../../mocks'; import { ValuesInput } from './values_input'; import type { TermsIndexPatternColumn } from '.'; @@ -60,6 +61,7 @@ const defaultProps = { dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), // need to provide the terms operation as some helpers use operation specific features diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx index b22369dfb2dd2..768783d5ce38c 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/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b75bcdabf49d9..4d84c8c840f0a 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -195,6 +195,7 @@ export interface LensPublicStart { openInNewTab?: boolean; originatingApp?: string; originatingPath?: string; + skipAppLeave?: boolean; } ) => void; /** @@ -458,7 +459,7 @@ export class LensPlugin { SaveModalComponent: getSaveModalComponent(core, startDependencies), navigateToPrefilledEditor: ( input, - { openInNewTab = false, originatingApp = '', originatingPath } = {} + { openInNewTab = false, originatingApp = '', originatingPath, skipAppLeave = false } = {} ) => { // for openInNewTab, we set the time range in url via getEditPath below if (input?.timeRange && !openInNewTab) { @@ -476,6 +477,7 @@ export class LensPlugin { originatingPath, valueInput: input, }, + skipAppLeave, }); }, canUseEditor: () => { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 1ffc300542b09..4c2f0785e7a3e 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/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 61df3bb458912..8de7aa009b54a 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -11,7 +11,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { schema } from '@kbn/config-schema'; import { RequestHandlerContext, ElasticsearchClient } from '@kbn/core/server'; import { CoreSetup, Logger } from '@kbn/core/server'; -import { RuntimeField } from '@kbn/data-plugin/common'; +import { RuntimeField } from '@kbn/data-views-plugin/common'; import { DataViewsService, DataView, FieldSpec } from '@kbn/data-views-plugin/common'; import { UI_SETTINGS } from '@kbn/data-plugin/server'; import { BASE_API_URL } from '../../common'; diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 20def97df7aed..df55e06710599 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -12,7 +12,6 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../task_manager/tsconfig.json" }, { "path": "../global_search/tsconfig.json" }, - { "path": "../ui_actions_enhanced/tsconfig.json" }, { "path": "../saved_objects_tagging/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/data_views/tsconfig.json" }, @@ -24,6 +23,7 @@ { "path": "../../../src/plugins/visualizations/tsconfig.json" }, { "path": "../../../src/plugins/dashboard/tsconfig.json" }, { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../../../src/plugins/share/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 53660c5256497..d2c69e89078af 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -195,6 +195,7 @@ export enum LAYER_STYLE_TYPE { VECTOR = 'VECTOR', HEATMAP = 'HEATMAP', TILE = 'TILE', + EMS_VECTOR_TILE = 'EMS_VECTOR_TILE', } export enum COLOR_MAP_TYPE { @@ -288,6 +289,7 @@ export enum DATA_MAPPING_FUNCTION { INTERPOLATE = 'INTERPOLATE', PERCENTILES = 'PERCENTILES', } + export const DEFAULT_PERCENTILES = [50, 75, 90, 95, 99]; export type RawValue = string | string[] | number | boolean | undefined | null; diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts index 5aba9ba06dc48..1456fd2acd25b 100644 --- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -11,6 +11,7 @@ import { Query } from '@kbn/data-plugin/public'; import { Feature } from 'geojson'; import { + EMSVectorTileStyleDescriptor, HeatmapStyleDescriptor, StyleDescriptor, VectorStyleDescriptor, @@ -83,3 +84,8 @@ export type HeatmapLayerDescriptor = LayerDescriptor & { type: LAYER_TYPE.HEATMAP; style: HeatmapStyleDescriptor; }; + +export type EMSVectorTileLayerDescriptor = LayerDescriptor & { + type: LAYER_TYPE.EMS_VECTOR_TILE; + style: EMSVectorTileStyleDescriptor; +}; diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts index dce4d0f9df50e..3b773d8998a5f 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts @@ -256,6 +256,10 @@ export type HeatmapStyleDescriptor = StyleDescriptor & { colorRampName: string; }; +export type EMSVectorTileStyleDescriptor = StyleDescriptor & { + color: string; +}; + export type StylePropertyOptions = | LabelBorderSizeOptions | SymbolizeAsOptions diff --git a/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.test.ts index 9c81b4c3aa72f..4af58615cf99d 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.test.ts @@ -89,7 +89,7 @@ describe('EMS is enabled', () => { lightModeDefault: 'road_map_desaturated', type: 'EMS_TMS', }, - style: { type: 'TILE' }, + style: { type: 'EMS_VECTOR_TILE', color: '' }, type: 'EMS_VECTOR_TILE', visible: true, }); diff --git a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts index 5f12f4cbc2b61..7f000804d91a5 100644 --- a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts @@ -8,7 +8,7 @@ import { SOURCE_TYPES } from '../../../../common/constants'; import { DataFilters, - LayerDescriptor, + EMSVectorTileLayerDescriptor, XYZTMSSourceDescriptor, } from '../../../../common/descriptor_types'; import { ILayer } from '../layer'; @@ -34,7 +34,7 @@ describe('EmsVectorTileLayer', () => { urlTemplate: 'https://example.com/{x}/{y}/{z}.png', id: 'mockSourceId', } as XYZTMSSourceDescriptor, - }, + } as unknown as EMSVectorTileLayerDescriptor, }); let actualMeta; @@ -59,7 +59,7 @@ describe('EmsVectorTileLayer', () => { test('should set locale to none for existing layers where locale is not defined', () => { const layer = new EmsVectorTileLayer({ source: {} as unknown as EMSTMSSource, - layerDescriptor: {} as unknown as LayerDescriptor, + layerDescriptor: {} as unknown as EMSVectorTileLayerDescriptor, }); expect(layer.getLocale()).toBe('none'); }); @@ -69,7 +69,7 @@ describe('EmsVectorTileLayer', () => { source: {} as unknown as EMSTMSSource, layerDescriptor: { locale: 'xx', - } as unknown as LayerDescriptor, + } as unknown as EMSVectorTileLayerDescriptor, }); expect(layer.getLocale()).toBe('xx'); }); @@ -79,7 +79,7 @@ describe('EmsVectorTileLayer', () => { test('should return false when tile loading has not started', () => { const layer = new EmsVectorTileLayer({ source: {} as unknown as EMSTMSSource, - layerDescriptor: {} as unknown as LayerDescriptor, + layerDescriptor: {} as unknown as EMSVectorTileLayerDescriptor, }); expect(layer.isInitialDataLoadComplete()).toBe(false); }); @@ -89,7 +89,7 @@ describe('EmsVectorTileLayer', () => { source: {} as unknown as EMSTMSSource, layerDescriptor: { __areTilesLoaded: false, - } as unknown as LayerDescriptor, + } as unknown as EMSVectorTileLayerDescriptor, }); expect(layer.isInitialDataLoadComplete()).toBe(false); }); @@ -99,7 +99,7 @@ describe('EmsVectorTileLayer', () => { source: {} as unknown as EMSTMSSource, layerDescriptor: { __areTilesLoaded: true, - } as unknown as LayerDescriptor, + } as unknown as EMSVectorTileLayerDescriptor, }); expect(layer.isInitialDataLoadComplete()).toBe(true); }); diff --git a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.ts similarity index 87% rename from x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx rename to x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.ts index 679629b838835..05009abc7793b 100644 --- a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.ts @@ -6,7 +6,7 @@ */ import type { Map as MbMap, LayerSpecification, StyleSpecification } from '@kbn/mapbox-gl'; -import { type EmsSpriteSheet, TMSService } from '@elastic/ems-client'; +import { type blendMode, type EmsSpriteSheet, TMSService } from '@elastic/ems-client'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; // @ts-expect-error @@ -17,14 +17,13 @@ import { NO_EMS_LOCALE, SOURCE_DATA_REQUEST_ID, LAYER_TYPE, - LAYER_STYLE_TYPE, } from '../../../../common/constants'; -import { LayerDescriptor } from '../../../../common/descriptor_types'; +import { EMSVectorTileLayerDescriptor } from '../../../../common/descriptor_types'; import { DataRequest } from '../../util/data_request'; import { isRetina } from '../../../util'; import { DataRequestContext } from '../../../actions'; import { EMSTMSSource } from '../../sources/ems_tms_source'; -import { TileStyle } from '../../styles/tile/tile_style'; +import { EMSVectorTileStyle } from '../../styles/ems/ems_vector_tile_style'; interface SourceRequestMeta { tileLayerId: string; @@ -40,26 +39,35 @@ interface SourceRequestData { } export class EmsVectorTileLayer extends AbstractLayer { - static createDescriptor(options: Partial) { - const tileLayerDescriptor = super.createDescriptor(options); - tileLayerDescriptor.type = LAYER_TYPE.EMS_VECTOR_TILE; - tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); - tileLayerDescriptor.locale = _.get(options, 'locale', AUTOSELECT_EMS_LOCALE); - tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; - return tileLayerDescriptor; - } + private readonly _style: EMSVectorTileStyle; - private readonly _style: TileStyle; + static createDescriptor( + options: Partial + ): EMSVectorTileLayerDescriptor { + const emsVectorTileLayerDescriptor = super.createDescriptor( + options + ) as EMSVectorTileLayerDescriptor; + emsVectorTileLayerDescriptor.type = LAYER_TYPE.EMS_VECTOR_TILE; + emsVectorTileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + emsVectorTileLayerDescriptor.locale = _.get(options, 'locale', AUTOSELECT_EMS_LOCALE); + emsVectorTileLayerDescriptor.style = EMSVectorTileStyle.createDescriptor(); + return emsVectorTileLayerDescriptor; + } constructor({ source, layerDescriptor, }: { source: EMSTMSSource; - layerDescriptor: LayerDescriptor; + layerDescriptor: EMSVectorTileLayerDescriptor; }) { super({ source, layerDescriptor }); - this._style = new TileStyle(); + if (!layerDescriptor.style) { + const defaultStyle = EMSVectorTileStyle.createDescriptor(); + this._style = new EMSVectorTileStyle(defaultStyle); + } else { + this._style = new EMSVectorTileStyle(layerDescriptor.style); + } } isInitialDataLoadComplete(): boolean { @@ -377,6 +385,26 @@ export class EmsVectorTileLayer extends AbstractLayer { return []; } + _setColorFilter(mbMap: MbMap, mbLayer: LayerSpecification, mbLayerId: string) { + const color = this.getCurrentStyle().getColor(); + + const colorOperation = TMSService.colorOperationDefaults.find(({ style }) => { + return style === this.getSource().getTileLayerId(); + }); + if (!colorOperation) return; + const { operation, percentage } = colorOperation; + + const properties = TMSService.transformColorProperties( + mbLayer, + color, + operation as unknown as blendMode, + percentage + ); + for (const { property, color: newColor } of properties) { + mbMap.setPaintProperty(mbLayerId, property, newColor); + } + } + _setOpacityForType(mbMap: MbMap, mbLayer: LayerSpecification, mbLayerId: string) { this._getOpacityProps(mbLayer.type).forEach((opacityProp) => { const mbPaint = mbLayer.paint as { [key: string]: unknown } | undefined; @@ -433,6 +461,7 @@ export class EmsVectorTileLayer extends AbstractLayer { this.syncVisibilityWithMb(mbMap, mbLayerId); this._setLayerZoomRange(mbMap, mbLayer, mbLayerId); this._setOpacityForType(mbMap, mbLayer, mbLayerId); + this._setColorFilter(mbMap, mbLayer, mbLayerId); this._setLanguage(mbMap, mbLayer, mbLayerId); }); } diff --git a/x-pack/plugins/maps/public/classes/styles/ems/components/__snapshots__/ems_vector_tile_style_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/ems/components/__snapshots__/ems_vector_tile_style_editor.test.tsx.snap new file mode 100644 index 0000000000000..3dc79d6537abf --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/ems/components/__snapshots__/ems_vector_tile_style_editor.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EMSVectorTileStyleEditor is rendered 1`] = ` + + + +`; diff --git a/x-pack/plugins/maps/public/classes/styles/ems/components/ems_vector_tile_style_editor.test.tsx b/x-pack/plugins/maps/public/classes/styles/ems/components/ems_vector_tile_style_editor.test.tsx new file mode 100644 index 0000000000000..4298cd4d0f525 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/ems/components/ems_vector_tile_style_editor.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EMSVectorTileStyleEditor } from './ems_vector_tile_style_editor'; + +describe('EMSVectorTileStyleEditor', () => { + test('is rendered', () => { + const component = shallow( + {}} /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/ems/components/ems_vector_tile_style_editor.tsx b/x-pack/plugins/maps/public/classes/styles/ems/components/ems_vector_tile_style_editor.tsx new file mode 100644 index 0000000000000..fc633e4a0c0ac --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/ems/components/ems_vector_tile_style_editor.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 React from 'react'; + +import { EuiFormRow, EuiColorPicker } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + color: string; + onColorChange: ({ color }: { color: string }) => void; +} + +export function EMSVectorTileStyleEditor({ color, onColorChange }: Props) { + const onChange = (selectedColor: string) => { + onColorChange({ + color: selectedColor, + }); + }; + return ( + + + + ); +} diff --git a/x-pack/plugins/maps/public/classes/styles/ems/ems_vector_tile_style.tsx b/x-pack/plugins/maps/public/classes/styles/ems/ems_vector_tile_style.tsx new file mode 100644 index 0000000000000..6e090caeeed8c --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/ems/ems_vector_tile_style.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { IStyle } from '../style'; +import { EMSVectorTileStyleDescriptor, StyleDescriptor } from '../../../../common/descriptor_types'; +import { LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { EMSVectorTileStyleEditor } from './components/ems_vector_tile_style_editor'; + +export class EMSVectorTileStyle implements IStyle { + readonly _descriptor: EMSVectorTileStyleDescriptor; + + constructor(descriptor: { color: string } = { color: '' }) { + this._descriptor = EMSVectorTileStyle.createDescriptor(descriptor.color); + } + + static createDescriptor(color?: string) { + return { + type: LAYER_STYLE_TYPE.EMS_VECTOR_TILE, + color: color ?? '', + }; + } + + getType() { + return LAYER_STYLE_TYPE.EMS_VECTOR_TILE; + } + + getColor() { + return this._descriptor.color; + } + + renderEditor(onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void) { + const onColorChange = ({ color }: { color: string }) => { + const styleDescriptor = EMSVectorTileStyle.createDescriptor(color); + onStyleDescriptorChange(styleDescriptor); + }; + + return ( + + ); + } +} diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index e65e4599d52c3..cbd1b3aa4b4c8 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IndexPatternsContract } from '@kbn/data-plugin/public'; +import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { AppMountParameters } from '@kbn/core/public'; import { IContainer } from '@kbn/embeddable-plugin/public'; import { LayerDescriptor } from '../../common/descriptor_types'; @@ -27,7 +27,7 @@ export interface LazyLoadedMapModules { initialInput: MapEmbeddableInput, parent?: IContainer ) => MapEmbeddableType; - getIndexPatternService: () => IndexPatternsContract; + getIndexPatternService: () => DataViewsContract; getMapsCapabilities: () => any; renderApp: (params: AppMountParameters, AppUsageTracker: React.FC) => Promise<() => void>; createSecurityLayerDescriptors: ( diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index d2ab0324b3941..d1e4f2f021ff5 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -42,6 +42,7 @@ import { DataRequestDescriptor, CustomIcon, DrawState, + EMSVectorTileLayerDescriptor, EditState, Goto, HeatmapLayerDescriptor, @@ -79,7 +80,10 @@ export function createLayerInstance( case LAYER_TYPE.RASTER_TILE: return new RasterTileLayer({ layerDescriptor, source: source as ITMSSource }); case LAYER_TYPE.EMS_VECTOR_TILE: - return new EmsVectorTileLayer({ layerDescriptor, source: source as EMSTMSSource }); + return new EmsVectorTileLayer({ + layerDescriptor: layerDescriptor as EMSVectorTileLayerDescriptor, + source: source as EMSTMSSource, + }); case LAYER_TYPE.HEATMAP: return new HeatmapLayer({ layerDescriptor: layerDescriptor as HeatmapLayerDescriptor, diff --git a/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts b/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts index 542e61f8af9a6..435e56a8b60a9 100644 --- a/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts +++ b/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient, IScopedClusterClient } from '@kbn/core/server'; -import { IndexPatternsCommonService } from '@kbn/data-plugin/server'; +import { DataViewsCommonService } from '@kbn/data-plugin/server'; import { CreateDocSourceResp, IndexSourceMappings, BodySettings } from '../../common/types'; import { MAPS_NEW_VECTOR_LAYER_META_CREATED_BY } from '../../common/constants'; @@ -21,7 +21,7 @@ export async function createDocSource( index: string, mappings: IndexSourceMappings, { asCurrentUser }: IScopedClusterClient, - indexPatternsService: IndexPatternsCommonService + indexPatternsService: DataViewsCommonService ): Promise { try { await createIndex(index, mappings, asCurrentUser); diff --git a/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts b/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts index 7f2eacc257fc5..847aadb447034 100644 --- a/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts +++ b/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts @@ -51,7 +51,7 @@ export function initIndexingRoutes({ async (context, request, response) => { const coreContext = await context.core; const { index, mappings } = request.body; - const indexPatternsService = await dataPlugin.indexPatterns.indexPatternsServiceFactory( + const indexPatternsService = await dataPlugin.indexPatterns.dataViewsServiceFactory( coreContext.savedObjects.client, coreContext.elasticsearch.client.asCurrentUser, request diff --git a/x-pack/plugins/maps/server/kibana_server_services.ts b/x-pack/plugins/maps/server/kibana_server_services.ts index b1dab885ed627..84cedeb721824 100644 --- a/x-pack/plugins/maps/server/kibana_server_services.ts +++ b/x-pack/plugins/maps/server/kibana_server_services.ts @@ -20,5 +20,5 @@ export const getSavedObjectClient = (extraTypes?: string[]) => { }; export const getIndexPatternsServiceFactory = () => - pluginsStart.data.indexPatterns.indexPatternsServiceFactory; + pluginsStart.data.indexPatterns.dataViewsServiceFactory; export const getElasticsearch = () => coreStart.elasticsearch; diff --git a/x-pack/plugins/ui_actions_enhanced/.storybook/main.js b/x-pack/plugins/ml/common/types/groups.ts similarity index 74% rename from x-pack/plugins/ui_actions_enhanced/.storybook/main.js rename to x-pack/plugins/ml/common/types/groups.ts index 86b48c32f103e..3d5820ccdb672 100644 --- a/x-pack/plugins/ui_actions_enhanced/.storybook/main.js +++ b/x-pack/plugins/ml/common/types/groups.ts @@ -5,4 +5,8 @@ * 2.0. */ -module.exports = require('@kbn/storybook').defaultConfig; +export interface Group { + id: string; + jobIds: string[]; + calendarIds: string[]; +} diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts index 113d77d7b16da..7d34615d61758 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { FIELD_FORMAT_IDS, FieldFormatParams } from '@kbn/field-formats-plugin/common'; import { useMlKibana } from './kibana_context'; /** * Set of reasonable defaults for formatters for the ML app. */ -const defaultParam = { +const defaultParam: Record = { [FIELD_FORMAT_IDS.DURATION]: { inputFormat: 'milliseconds', outputFormat: 'humanizePrecise', }, -} as Record; +}; export function useFieldFormatter(fieldType: FIELD_FORMAT_IDS) { const { diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline_state_service.ts b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline_state_service.ts index 34d3f0bdfc457..696e729423a95 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline_state_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline_state_service.ts @@ -732,7 +732,7 @@ export class AnomalyTimelineStateService extends StateService { public getSwimLaneBucketInterval$(): Observable { return this._swimLaneBucketInterval$.pipe( - filter((v): v is TimeBucketsInterval => !v), + filter((v): v is TimeBucketsInterval => !!v), distinctUntilChanged((prev, curr) => { return prev.asSeconds() === curr.asSeconds(); }) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx index 0b0c3228fd65f..47a64dd90d502 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx @@ -37,7 +37,7 @@ export const JobFilterBar: FC = ({ queryText, setFilters }) = const loadGroups = useCallback(async () => { try { const response = await mlApiServices.jobs.groups(); - return response.map((g: any) => ({ + return response.map((g) => ({ value: g.id, view: (
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index 12f72a8a40218..fc82184341c02 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -94,6 +94,7 @@ export const AdvancedDetectorModal: FC = ({ const [excludeFrequentEnabled, setExcludeFrequentEnabled] = useState(true); const [fieldOptionEnabled, setFieldOptionEnabled] = useState(true); const { descriptionPlaceholder, setDescriptionPlaceholder } = useDetectorPlaceholder(detector); + const [selectedFieldNames, setSelectedFieldNames] = useState([]); const usingScriptFields = jobCreator.additionalFields.length > 0; // list of aggregation combobox options. @@ -106,7 +107,7 @@ export const AdvancedDetectorModal: FC = ({ const { currentFieldOptions, setCurrentFieldOptions } = useCurrentFieldOptions( detector.agg, filterCategoryFields(jobCreator.additionalFields, false), - fields + selectedFieldNames ); const allFieldOptions: EuiComboBoxOptionOption[] = [ @@ -116,7 +117,9 @@ export const AdvancedDetectorModal: FC = ({ const splitFieldOptions: EuiComboBoxOptionOption[] = [ ...allFieldOptions, ...createMlcategoryFieldOption(jobCreator.categorizationFieldName), - ].sort(comboBoxOptionsSort); + ] + .sort(comboBoxOptionsSort) + .filter(({ label }) => selectedFieldNames.includes(label) === false); const eventRateField = fields.find((f) => f.id === EVENT_RATE_FIELD_ID); @@ -165,6 +168,13 @@ export const AdvancedDetectorModal: FC = ({ setFieldOptionEnabled(false); } + setSelectedFieldNames([ + ...(field ? [field.name] : []), + ...(byField ? [byField.name] : []), + ...(overField ? [overField.name] : []), + ...(partitionField ? [partitionField.name] : []), + ]); + const dtr: RichDetector = { agg, field, @@ -390,14 +400,16 @@ function createFieldOptionsFromAgg(agg: Aggregation | null, additionalFields: Fi function useCurrentFieldOptions( aggregation: Aggregation | null, additionalFields: Field[], - fields: Field[] + selectedFieldNames: string[] ) { const [currentFieldOptions, setCurrentFieldOptions] = useState( createFieldOptionsFromAgg(aggregation, additionalFields) ); return { - currentFieldOptions, + currentFieldOptions: currentFieldOptions.filter( + ({ label }) => selectedFieldNames.includes(label) === false + ), setCurrentFieldOptions: (agg: Aggregation | null) => setCurrentFieldOptions(createFieldOptionsFromAgg(agg, additionalFields)), }; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index f1492074ed762..250be3b73adfb 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -20,6 +20,7 @@ import type { import type { JobMessage } from '../../../../common/types/audit_message'; import type { JobAction } from '../../../../common/constants/job_actions'; import type { AggFieldNamePair, RuntimeMappings } from '../../../../common/types/fields'; +import type { Group } from '../../../../common/types/groups'; import type { ExistingJobsAndGroups } from '../job_service'; import type { CategorizationAnalyzer, @@ -83,7 +84,7 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }, groups() { - return httpService.http({ + return httpService.http({ path: `${ML_BASE_PATH}/jobs/groups`, method: 'GET', }); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts index 85f5917e2430b..b83bc814e1c5d 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts @@ -7,6 +7,7 @@ // Service for obtaining data for the ML Results dashboards. import type { ESSearchRequest, ESSearchResponse } from '@kbn/core/types/elasticsearch'; +import type { CriteriaField } from '../results_service'; import { HttpService } from '../http_service'; import { basePath } from '.'; import { JOB_ID, PARTITION_FIELD_VALUE } from '../../../../common/constants/anomalies'; @@ -17,7 +18,7 @@ import type { import type { JobId } from '../../../../common/types/anomaly_detection_jobs'; import type { PartitionFieldsDefinition } from '../results_service/result_service_rx'; import type { PartitionFieldsConfig } from '../../../../common/types/storage'; -import type { MLAnomalyDoc } from '../../../../common/types/anomalies'; +import type { AnomalyRecordDoc, MLAnomalyDoc } from '../../../../common/types/anomalies'; import type { EntityField } from '../../../../common/util/anomaly_utils'; import type { InfluencersFilterQuery } from '../../../../common/types/es_client'; import type { ExplorerChartsData } from '../../../../common/types/results'; @@ -193,4 +194,29 @@ export const resultsApiProvider = (httpService: HttpService) => ({ body, }); }, + + getAnomalyRecords$( + jobIds: string[], + criteriaFields: CriteriaField[], + severity: number, + earliestMs: number | null, + latestMs: number | null, + interval: string, + functionDescription?: string + ) { + const body = JSON.stringify({ + jobIds, + criteriaFields, + threshold: severity, + earliestMs, + latestMs, + interval, + functionDescription, + }); + return httpService.http$<{ success: boolean; records: AnomalyRecordDoc[] }>({ + path: `${basePath()}/results/anomaly_records`, + method: 'POST', + body, + }); + }, }); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index 80169d8914682..f9dd1fa94c4f0 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -8,10 +8,7 @@ import { forkJoin, Observable, of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { ml } from '../../services/ml_api_service'; -import { - ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, -} from '../../../../common/constants/search'; +import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../common/constants/search'; import { extractErrorMessage } from '../../../../common/util/errors'; import { mlTimeSeriesSearchService } from '../timeseries_search_service'; import { mlResultsService, CriteriaField } from '../../services/results_service'; @@ -71,13 +68,13 @@ export function getFocusData( esFunctionToPlotIfMetric ), // Query 2 - load all the records across selected time range for the chart anomaly markers. - mlResultsService.getRecordsForCriteria( + ml.results.getAnomalyRecords$( [selectedJob.job_id], criteriaFields, 0, searchBounds.min.valueOf(), searchBounds.max.valueOf(), - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, + focusAggregationInterval.expression, functionDescription ), // Query 3 - load any scheduled events for the selected job. diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json index 4d19cdc9f533d..5659580a03653 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json @@ -23,7 +23,7 @@ "date" : { "date_histogram" : { "field" : "@timestamp", - "fixed_interval" : "90s" + "fixed_interval" : "60s" } } }, diff --git a/x-pack/plugins/ml/server/models/job_service/groups.ts b/x-pack/plugins/ml/server/models/job_service/groups.ts index c898e10f424a7..57c6464fe9530 100644 --- a/x-pack/plugins/ml/server/models/job_service/groups.ts +++ b/x-pack/plugins/ml/server/models/job_service/groups.ts @@ -7,14 +7,9 @@ import { CalendarManager } from '../calendar'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; +import type { Group } from '../../../common/types/groups'; import type { MlClient } from '../../lib/ml_client'; -export interface Group { - id: string; - jobIds: string[]; - calendarIds: string[]; -} - export interface Results { [id: string]: { success: boolean; diff --git a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts index 725fed25f1bce..dd7e2b3373e89 100644 --- a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts +++ b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts @@ -1642,7 +1642,14 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu criteria = criteria.concat(config.entityFields); try { - return await getRecordsForCriteria([config.jobId], criteria, 0, range.min, range.max, 500); + return await getRecordsForCriteria( + [config.jobId], + criteria, + 0, + range.min, + range.max, + config.interval + ); } catch (error) { handleError( i18n.translate('xpack.ml.timeSeriesJob.recordsForCriteriaErrorMessage', { @@ -1655,33 +1662,36 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu } /** - * TODO make an endpoint + * Fetches anomaly records aggregating on the chart interval. + * * @param jobIds * @param criteriaFields * @param threshold * @param earliestMs * @param latestMs - * @param maxResults + * @param interval * @param functionDescription */ async function getRecordsForCriteria( jobIds: string[], criteriaFields: CriteriaField[], - threshold: any, + threshold: number, earliestMs: number | null, latestMs: number | null, - maxResults: number | undefined, + interval: string, functionDescription?: string ): Promise { const obj: RecordsForCriteria = { success: true, records: [] }; // Build the criteria to use in the bool filter part of the request. // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria: any[] = [ + const boolCriteria: estypes.QueryDslQueryContainer[] = [ { range: { timestamp: { + // @ts-ignore gte: earliestMs, + // @ts-ignore lte: latestMs, format: 'epoch_millis', }, @@ -1736,14 +1746,13 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu } const searchRequest: estypes.SearchRequest = { - size: maxResults !== undefined ? maxResults : 100, + size: 0, query: { bool: { filter: [ { - query_string: { - query: 'result_type:record', - analyze_wildcard: false, + term: { + result_type: 'record', }, }, { @@ -1754,18 +1763,42 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu ], }, }, - // @ts-ignore check score request - sort: [{ record_score: { order: 'desc' } }], + aggs: { + anomalies_over_time: { + date_histogram: { + field: 'timestamp', + fixed_interval: interval, + // Ignore empty buckets + min_doc_count: 1, + }, + aggs: { + top_records: { + top_hits: { + size: 1, + sort: [{ record_score: { order: 'desc' } }], + }, + }, + }, + }, + }, }; const resp = await mlClient.anomalySearch(searchRequest, jobIds); - // @ts-ignore - if (resp.hits.total.value > 0) { - each(resp.hits.hits, (hit: any) => { - obj.records.push(hit._source); - }); - } + const records = ( + ( + resp.aggregations!.anomalies_over_time as estypes.AggregationsMultiBucketAggregateBase<{ + top_records: estypes.AggregationsTopHitsAggregate; + }> + ).buckets as Array<{ top_records: estypes.AggregationsTopHitsAggregate }> + ) + .map((b) => { + return b.top_records.hits.hits[0]?._source; + }) + .filter(isDefined); + + obj.records = records; + return obj; } @@ -1942,5 +1975,8 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu return chartData; } - return getAnomalyChartsData; + return { + getAnomalyChartsData, + getRecordsForCriteria, + }; } diff --git a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index 189aa99e24ce5..48ec6bf9454b5 100644 --- a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -6,6 +6,7 @@ */ import Boom from '@hapi/boom'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { PARTITION_FIELDS } from '../../../common/constants/anomalies'; import { PartitionFieldsType } from '../../../common/types/anomalies'; import { CriteriaField } from './results_service'; @@ -18,6 +19,11 @@ type SearchTerm = } | undefined; +export interface PartitionFieldData { + name: string; + values: Array<{ value: string; maxRecordScore?: number }>; +} + /** * Gets an object for aggregation query to retrieve field name and values. * @param fieldType - Field type @@ -110,23 +116,38 @@ function getFieldAgg( * @param fieldType - Field type * @param aggs - Aggregation response */ -function getFieldObject(fieldType: PartitionFieldsType, aggs: any) { - const fieldNameKey = `${fieldType}_name`; - const fieldValueKey = `${fieldType}_value`; +function getFieldObject( + fieldType: PartitionFieldsType, + aggs: Record +): Record | {} { + const fieldNameKey = `${fieldType}_name` as const; + const fieldValueKey = `${fieldType}_value` as const; + + const fieldNameAgg = aggs[fieldNameKey] as estypes.AggregationsMultiTermsAggregate; + const fieldValueAgg = aggs[fieldValueKey] as unknown as { + values: estypes.AggregationsMultiBucketAggregateBase<{ + key: string; + maxRecordScore?: { value: number }; + }>; + }; - return aggs[fieldNameKey].buckets.length > 0 + return Array.isArray(fieldNameAgg.buckets) && fieldNameAgg.buckets.length > 0 ? { [fieldType]: { - name: aggs[fieldNameKey].buckets[0].key, - values: aggs[fieldValueKey].values.buckets.map(({ key, maxRecordScore }: any) => ({ - value: key, - ...(maxRecordScore ? { maxRecordScore: maxRecordScore.value } : {}), - })), + name: fieldNameAgg.buckets[0].key, + values: Array.isArray(fieldValueAgg.values.buckets) + ? fieldValueAgg.values.buckets.map(({ key, maxRecordScore }) => ({ + value: key, + ...(maxRecordScore ? { maxRecordScore: maxRecordScore.value } : {}), + })) + : [], }, } : {}; } +export type PartitionFieldValueResponse = Record; + export const getPartitionFieldsValuesFactory = (mlClient: MlClient) => /** * Gets the record of partition fields with possible values that fit the provided queries. @@ -144,7 +165,7 @@ export const getPartitionFieldsValuesFactory = (mlClient: MlClient) => earliestMs: number, latestMs: number, fieldsConfig: FieldsConfig = {} - ) { + ): Promise { const jobsResponse = await mlClient.getJobs({ job_id: jobId }); if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) { throw Boom.notFound(`Job with the id "${jobId}" not found`); @@ -152,7 +173,7 @@ export const getPartitionFieldsValuesFactory = (mlClient: MlClient) => const job = jobsResponse.jobs[0]; - const isModelPlotEnabled = job?.model_plot_config?.enabled; + const isModelPlotEnabled = !!job?.model_plot_config?.enabled; const isAnomalousOnly = (Object.entries(fieldsConfig) as Array<[string, FieldConfig]>).some( ([k, v]) => { return !!v?.anomalousOnly; @@ -165,14 +186,14 @@ export const getPartitionFieldsValuesFactory = (mlClient: MlClient) => } ); - const isModelPlotSearch = !!isModelPlotEnabled && !isAnomalousOnly; + const isModelPlotSearch = isModelPlotEnabled && !isAnomalousOnly; // Remove the time filter in case model plot is not enabled // and time range is not applied, so // it includes the records that occurred as anomalies historically const searchAllTime = !isModelPlotEnabled && !applyTimeRange; - const requestBody = { + const requestBody: estypes.SearchRequest['body'] = { query: { bool: { filter: [ @@ -230,7 +251,7 @@ export const getPartitionFieldsValuesFactory = (mlClient: MlClient) => return PARTITION_FIELDS.reduce((acc, key) => { return { ...acc, - ...getFieldObject(key, body.aggregations), + ...getFieldObject(key, body.aggregations!), }; }, {}); }; diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index b84b693fcc111..5af471bf44997 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -799,6 +799,11 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust return finalResults; } + const { getAnomalyChartsData, getRecordsForCriteria } = anomalyChartsDataProvider( + mlClient, + client! + ); + return { getAnomaliesTableData, getCategoryDefinition, @@ -809,6 +814,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust getCategorizerStats, getCategoryStoppedPartitions, getDatafeedResultsChartData, - getAnomalyChartsData: anomalyChartsDataProvider(mlClient, client!), + getAnomalyChartsData, + getRecordsForCriteria, }; } diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index e36e51aded4ba..4cbec75ea97bf 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -56,6 +56,7 @@ "GetCategorizerStats", "GetCategoryStoppedPartitions", "GetAnomalyChartsData", + "GetAnomalyRecords", "Modules", "DataRecognizer", diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index a04eee11cbf10..3af4a570ce073 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -16,6 +16,7 @@ import { partitionFieldValuesSchema, anomalySearchSchema, getAnomalyChartsSchema, + getAnomalyRecordsSchema, } from './schemas/results_service_schema'; import { resultsServiceProvider } from '../models/results_service'; import { jobIdSchema } from './schemas/anomaly_detectors_schema'; @@ -422,4 +423,47 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization } }) ); + + /** + * @apiGroup ResultsService + * + * @api {post} /api/ml/results/anomaly_records Get anomaly records for criteria + * @apiName GetAnomalyRecords + * @apiDescription Returns anomaly records + * + * @apiSchema (body) getAnomalyRecordsSchema + */ + router.post( + { + path: '/api/ml/results/anomaly_records', + validate: { + body: getAnomalyRecordsSchema, + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { + try { + const { getRecordsForCriteria } = resultsServiceProvider(mlClient, client); + + const { jobIds, criteriaFields, earliestMs, latestMs, threshold, interval } = request.body; + + const resp = await getRecordsForCriteria( + jobIds, + criteriaFields, + threshold, + earliestMs, + latestMs, + interval + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts index e3b2bc84eb861..e2040aa399f64 100644 --- a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts @@ -136,3 +136,12 @@ export const getAnomalyChartsSchema = schema.object({ max: schema.maybe(schema.number()), }), }); + +export const getAnomalyRecordsSchema = schema.object({ + jobIds: schema.arrayOf(schema.string()), + threshold: schema.number({ defaultValue: 0, min: 0, max: 99 }), + earliestMs: schema.number(), + latestMs: schema.number(), + criteriaFields: schema.arrayOf(schema.any()), + interval: schema.string(), +}); diff --git a/x-pack/plugins/monitoring/common/http_api/_health/index.ts b/x-pack/plugins/monitoring/common/http_api/_health/index.ts new file mode 100644 index 0000000000000..50fdb597813ec --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/_health/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 * as rt from 'io-ts'; +import { numberFromStringRT, timestampFromStringRT } from '../shared'; + +export const getHealthRequestQueryRT = rt.partial({ + min: timestampFromStringRT, + max: timestampFromStringRT, + timeout: numberFromStringRT, +}); diff --git a/x-pack/plugins/monitoring/common/http_api/shared/index.ts b/x-pack/plugins/monitoring/common/http_api/shared/index.ts index 4e47c7239e39f..44b103d046824 100644 --- a/x-pack/plugins/monitoring/common/http_api/shared/index.ts +++ b/x-pack/plugins/monitoring/common/http_api/shared/index.ts @@ -10,5 +10,6 @@ export * from './cluster'; export * from './literal_value'; export * from './pagination'; export * from './query_string_boolean'; +export * from './query_string_number'; export * from './sorting'; export * from './time_range'; diff --git a/x-pack/plugins/monitoring/common/http_api/shared/query_string_number.test.ts b/x-pack/plugins/monitoring/common/http_api/shared/query_string_number.test.ts new file mode 100644 index 0000000000000..30d2167eef586 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/shared/query_string_number.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { either } from 'fp-ts'; +import { numberFromStringRT } from './query_string_number'; + +describe('NumberFromString runtime type', () => { + it('decodes strings to numbers', () => { + expect(numberFromStringRT.decode('123')).toEqual(either.right(123)); + expect(numberFromStringRT.decode('0')).toEqual(either.right(0)); + }); + + it('rejects when not a number', () => { + expect(either.isLeft(numberFromStringRT.decode(''))).toBeTruthy(); + expect(either.isLeft(numberFromStringRT.decode('ab12'))).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/monitoring/common/http_api/shared/query_string_number.ts b/x-pack/plugins/monitoring/common/http_api/shared/query_string_number.ts new file mode 100644 index 0000000000000..24305690dad08 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/shared/query_string_number.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const numberFromStringRT = new rt.Type( + 'NumberFromString', + rt.number.is, + (value, context) => { + const nb = parseInt(value as string, 10); + return isNaN(nb) ? rt.failure(value, context) : rt.success(nb); + }, + String +); diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index a6b91f22ae563..977753de42d97 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -6,6 +6,7 @@ */ export interface ElasticsearchResponse { + timed_out?: boolean; hits?: { hits: ElasticsearchResponseHit[]; total: { diff --git a/x-pack/plugins/monitoring/dev_docs/runbook/diagnostic_queries.md b/x-pack/plugins/monitoring/dev_docs/runbook/diagnostic_queries.md index 220b87e0490f8..507a3495306c8 100644 --- a/x-pack/plugins/monitoring/dev_docs/runbook/diagnostic_queries.md +++ b/x-pack/plugins/monitoring/dev_docs/runbook/diagnostic_queries.md @@ -1,7 +1,7 @@ If the stack monitoring UI isn't showing data for any cluster, it may first be useful to survey the available data using a query like this: ```Kibana Dev Tools -POST .monitoring-*/_search +POST .monitoring-*,*:.monitoring-*,metrics-*,*:metrics-*/_search { "size": 0, "query": { @@ -61,7 +61,10 @@ POST .monitoring-*/_search This will show what document types are available in each index for each cluster UUID in the last hour. -The main cluster list requires ES cluster stats to be available. You can use this query to check for the presence of cluster stats for a given `CLUSTER_UUID` (note the replacement required in the query). +The main cluster list requires ES cluster stats to be available. You can use this query to check for the presence of cluster stats for a given cluster. + +> **Note** +> `` in the query below must be replaced with the elasticsearch cluster UUID. This is available from the `cluster_uuid` key of the `GET /` response. ```Kibana Dev Tools POST .monitoring-*,*:.monitoring-*,metrics-*,*:metrics-*/_search diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/README.md b/x-pack/plugins/monitoring/server/routes/api/v1/_health/README.md new file mode 100644 index 0000000000000..21d6cff557e28 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/README.md @@ -0,0 +1,13 @@ +### Stack Monitoring Health API + +A endpoint that makes a handful of pre-determined queries to determine the health/status of stack monitoring for the configured kibana. + +GET /api/monitoring/v1/_health +parameters: +- (optional) min: start date of the queries, in ms or YYYY-MM-DD hh:mm:ss +- (optional) max: end date of the queries, in ms or YYYY-MM-DD hh:mm:ss +- (optional) timeout: maximum timeout of the queries, in seconds + +The response includes sections that can provide useful informations in a debugging context: +- settings: a subset of the kibana.yml settings relevant to stack monitoring +- monitoredClusters: a representation of the monitoring documents available to the running kibana. It exposes which metricsets are collected by what collection mode and when was the last time it was ingested. The query groups the metricsets by products and can help identifying missing documents that could explain why a page is not loading or crashing diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/index.ts new file mode 100644 index 0000000000000..32f3bcaa90237 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/index.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LegacyRequest, MonitoringCore } from '../../../../types'; +import { MonitoringConfig } from '../../../../config'; +import { createValidationFunction } from '../../../../lib/create_route_validation_function'; +import { getHealthRequestQueryRT } from '../../../../../common/http_api/_health'; +import { TimeRange } from '../../../../../common/http_api/shared'; +import { INDEX_PATTERN, INDEX_PATTERN_ENTERPRISE_SEARCH } from '../../../../../common/constants'; + +import { fetchMonitoredClusters } from './monitored_clusters'; + +const DEFAULT_QUERY_TIMERANGE = { min: 'now-15m', max: 'now' }; +const DEFAULT_QUERY_TIMEOUT_SECONDS = 15; + +export function registerV1HealthRoute(server: MonitoringCore) { + const validateQuery = createValidationFunction(getHealthRequestQueryRT); + + const withCCS = (indexPattern: string) => { + if (server.config.ui.ccs.enabled) { + return `${indexPattern},*:${indexPattern}`; + } + return indexPattern; + }; + + server.route({ + method: 'get', + path: '/api/monitoring/v1/_health', + validate: { + query: validateQuery, + }, + async handler(req: LegacyRequest) { + const logger = req.getLogger(); + const timeRange = { + min: req.query.min || DEFAULT_QUERY_TIMERANGE.min, + max: req.query.max || DEFAULT_QUERY_TIMERANGE.max, + } as TimeRange; + const timeout = req.query.timeout || DEFAULT_QUERY_TIMEOUT_SECONDS; + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + + const settings = extractSettings(server.config); + + const monitoredClusters = await fetchMonitoredClusters({ + timeout, + timeRange, + monitoringIndex: withCCS(INDEX_PATTERN), + entSearchIndex: withCCS(INDEX_PATTERN_ENTERPRISE_SEARCH), + search: (params: any) => callWithRequest(req, 'search', params), + logger, + }).catch((err: Error) => { + logger.error(`_health: failed to retrieve monitored clusters:\n${err.stack}`); + return { error: err.message }; + }); + + return { monitoredClusters, settings }; + }, + }); +} + +function extractSettings(config: MonitoringConfig) { + return { + ccs: config.ui.ccs.enabled, + logsIndex: config.ui.logs.index, + metricbeatIndex: config.ui.metricbeat.index, + hasRemoteClusterConfigured: (config.ui.elasticsearch.hosts || []).some(Boolean), + }; +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.test.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.test.ts new file mode 100644 index 0000000000000..9248af4cae823 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import assert from 'assert'; +import sinon from 'sinon'; +import { buildMonitoredClusters } from './build_monitored_clusters'; + +describe(__filename, () => { + describe('buildMonitoringClusters', () => { + test('it should build a representation of the monitoring state', () => { + const clustersBuckets = [ + { + key: 'cluster_one', + elasticsearch: { + buckets: [ + { + key: 'node_one', + shard: { + by_index: { + buckets: [ + { + key: '.ds-.monitoring-es-8-mb.2022', + last_seen: { + value: 123, + value_as_string: '2022-01-01', + }, + }, + ], + }, + }, + }, + ], + }, + }, + ]; + + const logger = { warn: sinon.spy() } as unknown as Logger; + const monitoredClusters = buildMonitoredClusters(clustersBuckets, logger); + assert.deepEqual(monitoredClusters, { + cluster_one: { + elasticsearch: { + node_one: { + shard: { + 'metricbeat-8': { + index: '.ds-.monitoring-es-8-mb.2022', + lastSeen: '2022-01-01', + }, + }, + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.ts new file mode 100644 index 0000000000000..df959c499d4b2 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import { isEmpty, mapValues, merge, omitBy, reduce } from 'lodash'; + +enum CollectionMode { + Internal = 'internal-monitoring', + Metricbeat7 = 'metricbeat-7', + Metricbeat8 = 'metricbeat-8', + Unknown = 'unknown', +} + +enum MonitoredProduct { + Cluster = 'cluster', + Elasticsearch = 'elasticsearch', + Kibana = 'kibana', + Beats = 'beats', + Logstash = 'logstash', + EnterpriseSearch = 'enterpriseSearch', +} + +interface MonitoredMetricsets { + [metricset: string]: { + [collectionMode in CollectionMode]: { + index: string; + lastSeen: string; + }; + }; +} + +interface MonitoredEntities { + [entityId: string]: MonitoredMetricsets; +} + +type MonitoredProducts = { + [product in MonitoredProduct]: MonitoredEntities; +}; + +export interface MonitoredClusters { + [clusterUuid: string]: MonitoredProducts; +} + +const internalMonitoringPattern = /^\.monitoring-(es|kibana|beats|logstash)-7-[0-9]{4}\..*/; +const metricbeatMonitoring7Pattern = /^\.monitoring-(es|kibana|beats|logstash|ent-search)-7.*-mb.*/; +const metricbeatMonitoring8Pattern = + /^\.ds-\.monitoring-(es|kibana|beats|logstash|ent-search)-8-mb.*/; + +const getCollectionMode = (index: string): CollectionMode => { + if (internalMonitoringPattern.test(index)) return CollectionMode.Internal; + if (metricbeatMonitoring7Pattern.test(index)) return CollectionMode.Metricbeat7; + if (metricbeatMonitoring8Pattern.test(index)) return CollectionMode.Metricbeat8; + + return CollectionMode.Unknown; +}; + +/** + * builds a normalized representation of the monitoring state from the provided + * query buckets with a cluster->product->entity->metricset hierarchy where + * cluster: the monitored cluster identifier + * product: the monitored products (eg elasticsearch) + * entity: the product unit of work (eg node) + * metricset: the collected metricsets for a given entity + * + * example: + * { + * "f-05NylTQg2G7rQXHnvYbA": { + * "elasticsearch": { + * "9NXA8Ov5QCeWAalKIHWFJg": { + * "shard": { + * "metricbeat-8": { + * "index": ".ds-.monitoring-es-8-mb-2022.05.17-000001", + * "lastSeen": "2022-05-17T16:56:52.929Z" + * } + * } + * } + * } + * } + * } + */ +export const buildMonitoredClusters = ( + clustersBuckets: any[], + logger: Logger +): MonitoredClusters => { + return clustersBuckets.reduce((clusters, { key, doc_count: _, ...products }) => { + clusters[key] = buildMonitoredProducts(products, logger); + return clusters; + }, {}); +}; + +/** + * some products may not have a common identifier for their entities across the + * metricsets and can create multiple aggregations. we make sure to merge these + * so the output only includes a single product entry + * we assume each aggregation is named as /productname(_aggsuffix)?/ + */ +const buildMonitoredProducts = (rawProducts: any, logger: Logger): MonitoredProducts => { + const validProducts = Object.values(MonitoredProduct); + const products = mapValues(rawProducts, (value, key) => { + if (!validProducts.some((product) => key.startsWith(product))) { + logger.warn(`buildMonitoredProducts: ignoring unknown product aggregation key (${key})`); + return {}; + } + + return buildMonitoredEntities(value.buckets); + }); + + return reduce( + products, + (uniqProducts: any, entities: any, aggregationKey: string) => { + if (isEmpty(entities)) return uniqProducts; + + const product = aggregationKey.split('_')[0]; + uniqProducts[product] = merge(uniqProducts[product], entities); + return uniqProducts; + }, + {} + ); +}; + +const buildMonitoredEntities = (entitiesBuckets: any[]): MonitoredEntities => { + return entitiesBuckets.reduce( + (entities, { key, key_as_string: keyAsString, doc_count: _, ...metricsets }) => { + entities[keyAsString || key] = buildMonitoredMetricsets(metricsets); + return entities; + }, + {} + ); +}; + +const buildMonitoredMetricsets = (rawMetricsets: any): MonitoredMetricsets => { + const monitoredMetricsets = mapValues( + rawMetricsets, + ({ by_index: byIndex }: { by_index: { buckets: any[] } }) => { + return byIndex.buckets.reduce((metricsets, { key, last_seen: lastSeen }) => { + metricsets[getCollectionMode(key)] = { + index: key, + lastSeen: lastSeen.value_as_string, + }; + return metricsets; + }, {}); + } + ); + + return omitBy(monitoredMetricsets, isEmpty) as unknown as MonitoredMetricsets; +}; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/fetch_monitored_clusters.test.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/fetch_monitored_clusters.test.ts new file mode 100644 index 0000000000000..fabece8a6b207 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/fetch_monitored_clusters.test.ts @@ -0,0 +1,328 @@ +/* + * 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 assert from 'assert'; +import sinon from 'sinon'; +import type { Logger } from '@kbn/core/server'; +import { fetchMonitoredClusters } from './fetch_monitored_clusters'; + +const getMockLogger = () => + ({ + warn: sinon.spy(), + error: sinon.spy(), + } as unknown as Logger); + +describe(__filename, () => { + describe('fetchMonitoringClusters', () => { + test('it should send multiple search queries', async () => { + const searchFn = jest.fn().mockResolvedValue({ + aggregations: { + clusters: { + buckets: [], + }, + }, + }); + + await fetchMonitoredClusters({ + timeout: 10, + monitoringIndex: 'foo', + entSearchIndex: 'foo', + timeRange: { min: 1652979091217, max: 11652979091217 }, + search: searchFn, + logger: getMockLogger(), + }); + + assert.equal(searchFn.mock.calls.length, 3); + }); + + test('it should report request timeouts', async () => { + const searchFn = jest + .fn() + .mockResolvedValueOnce({ + timed_out: false, + aggregations: {}, + }) + .mockResolvedValueOnce({ + timed_out: true, + aggregations: {}, + }) + .mockResolvedValueOnce({ + timed_out: false, + aggregations: {}, + }); + + const result = await fetchMonitoredClusters({ + timeout: 10, + monitoringIndex: 'foo', + entSearchIndex: 'foo', + timeRange: { min: 1652979091217, max: 11652979091217 }, + search: searchFn, + logger: getMockLogger(), + }); + + assert.equal(result.execution.timedOut, true); + }); + + test('it should report request errors', async () => { + const searchFn = jest + .fn() + .mockResolvedValueOnce({ + timed_out: false, + aggregations: {}, + }) + .mockRejectedValueOnce(new Error('massive failure')) + .mockResolvedValueOnce({ + timed_out: false, + aggregations: {}, + }); + + const result = await fetchMonitoredClusters({ + timeout: 10, + monitoringIndex: 'foo', + entSearchIndex: 'foo', + timeRange: { min: 1652979091217, max: 11652979091217 }, + search: searchFn, + logger: getMockLogger(), + }); + + assert.equal(result.execution.timedOut, false); + assert.deepEqual(result.execution.errors, ['massive failure']); + }); + + test('it should merge the query results', async () => { + const mainMetricsetsResponse = { + aggregations: { + clusters: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cluster-id.1', + doc_count: 11874, + elasticsearch: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'es-node-id.1', + doc_count: 540, + enrich: { + meta: {}, + doc_count: 180, + by_index: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '.ds-.monitoring-es-8-mb-2022.05.19-000001', + doc_count: 180, + last_seen: { + value: 1652975511716, + value_as_string: '2022-05-19T15:51:51.716Z', + }, + }, + ], + }, + }, + }, + ], + }, + + kibana: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'kibana-node-id.1', + doc_count: 540, + stats: { + meta: {}, + doc_count: 180, + by_index: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '.ds-.monitoring-kibana-8-mb-2022.05.19-000001', + doc_count: 162, + last_seen: { + value: 1652975513680, + value_as_string: '2022-05-19T15:51:53.680Z', + }, + }, + ], + }, + }, + }, + ], + }, + }, + ], + }, + }, + }; + + const persistentMetricsetsResponse = { + aggregations: { + clusters: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cluster-id.1', + doc_count: 11874, + elasticsearch: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'es-node-id.1', + doc_count: 540, + shard: { + meta: {}, + doc_count: 180, + by_index: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '.ds-.monitoring-es-8-mb-2022.05.19-000001', + doc_count: 180, + last_seen: { + value: 1652975511716, + value_as_string: '2022-05-19T15:51:51.716Z', + }, + }, + ], + }, + }, + }, + ], + }, + }, + ], + }, + }, + }; + + const entsearchMetricsetsResponse = { + aggregations: { + clusters: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cluster-id.1', + doc_count: 11874, + enterpriseSearch: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'ent-search-node-id.1', + doc_count: 540, + health: { + meta: {}, + doc_count: 180, + by_index: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '.ds-.monitoring-ent-search-8-mb-2022.05.19-000001', + doc_count: 180, + last_seen: { + value: 1652975511716, + value_as_string: '2022-05-19T15:51:51.716Z', + }, + }, + ], + }, + }, + }, + ], + }, + }, + ], + }, + }, + }; + + const searchFn = jest + .fn() + .mockResolvedValueOnce(mainMetricsetsResponse) + .mockResolvedValueOnce(persistentMetricsetsResponse) + .mockResolvedValueOnce(entsearchMetricsetsResponse); + + const monitoredClusters = await fetchMonitoredClusters({ + timeout: 10, + monitoringIndex: 'foo', + entSearchIndex: 'foo', + timeRange: { min: 1652979091217, max: 11652979091217 }, + search: searchFn, + logger: getMockLogger(), + }); + + assert.deepEqual(monitoredClusters, { + execution: { + timedOut: false, + errors: [], + }, + + clusters: { + 'cluster-id.1': { + elasticsearch: { + 'es-node-id.1': { + enrich: { + 'metricbeat-8': { + index: '.ds-.monitoring-es-8-mb-2022.05.19-000001', + lastSeen: '2022-05-19T15:51:51.716Z', + }, + }, + shard: { + 'metricbeat-8': { + index: '.ds-.monitoring-es-8-mb-2022.05.19-000001', + lastSeen: '2022-05-19T15:51:51.716Z', + }, + }, + }, + }, + + kibana: { + 'kibana-node-id.1': { + stats: { + 'metricbeat-8': { + index: '.ds-.monitoring-kibana-8-mb-2022.05.19-000001', + lastSeen: '2022-05-19T15:51:53.680Z', + }, + }, + }, + }, + + enterpriseSearch: { + 'ent-search-node-id.1': { + health: { + 'metricbeat-8': { + index: '.ds-.monitoring-ent-search-8-mb-2022.05.19-000001', + lastSeen: '2022-05-19T15:51:51.716Z', + }, + }, + }, + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/fetch_monitored_clusters.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/fetch_monitored_clusters.ts new file mode 100644 index 0000000000000..95f56aea5f625 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/fetch_monitored_clusters.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import { merge } from 'lodash'; + +import { ElasticsearchResponse } from '../../../../../../common/types/es'; +import { TimeRange } from '../../../../../../common/http_api/shared'; + +import { buildMonitoredClusters, MonitoredClusters } from './build_monitored_clusters'; +import { + monitoredClustersQuery, + persistentMetricsetsQuery, + enterpriseSearchQuery, +} from './monitored_clusters_query'; + +type SearchFn = (params: any) => Promise; + +interface MonitoredClustersResponse { + clusters?: MonitoredClusters; + execution: { + timedOut?: boolean; + errors?: string[]; + }; +} + +/** + * executes multiple search requests to build a representation of the monitoring + * documents. the queries aggregations are built with a similar hierarchy so + * we can merge them to a single output + */ +export const fetchMonitoredClusters = async ({ + timeout, + monitoringIndex, + entSearchIndex, + timeRange, + search, + logger, +}: { + timeout: number; + timeRange: TimeRange; + monitoringIndex: string; + entSearchIndex: string; + search: SearchFn; + logger: Logger; +}): Promise => { + const getMonitoredClusters = async ( + index: string, + body: any + ): Promise => { + try { + const { aggregations, timed_out: timedOut } = await search({ + index, + body, + size: 0, + ignore_unavailable: true, + }); + + const buckets = aggregations?.clusters?.buckets ?? []; + return { + clusters: buildMonitoredClusters(buckets, logger), + execution: { timedOut }, + }; + } catch (err) { + logger.error(`fetchMonitoredClusters: failed to fetch:\n${err.stack}`); + return { execution: { errors: [err.message] } }; + } + }; + + const results = await Promise.all([ + getMonitoredClusters(monitoringIndex, monitoredClustersQuery({ timeRange, timeout })), + getMonitoredClusters(monitoringIndex, persistentMetricsetsQuery({ timeout })), + getMonitoredClusters(entSearchIndex, enterpriseSearchQuery({ timeRange, timeout })), + ]); + + return { + clusters: merge({}, ...results.map(({ clusters }) => clusters)), + + execution: results + .map(({ execution }) => execution) + .reduce( + (acc, execution) => { + return { + timedOut: Boolean(acc.timedOut || execution.timedOut), + errors: acc.errors!.concat(execution.errors || []), + }; + }, + { + timedOut: false, + errors: [], + } + ), + }; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/index.ts similarity index 78% rename from x-pack/plugins/ui_actions_enhanced/public/components/index.ts rename to x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/index.ts index cf5e330e1f6de..b8282afe3b43d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './action_wizard'; +export { fetchMonitoredClusters } from './fetch_monitored_clusters'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts new file mode 100644 index 0000000000000..825cabc723294 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts @@ -0,0 +1,480 @@ +/* + * 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 { TimeRange } from '../../../../../../common/http_api/shared'; + +const MAX_BUCKET_SIZE = 100; + +interface QueryOptions { + timeRange?: TimeRange; + timeout: number; // in seconds +} + +/** + * returns a nested aggregation of the monitored products per cluster, standalone + * included. each product aggregation retrieves the related metricsets and the + * last time they were ingested. + * if a product requires multiple aggregations the key is suffixed with an identifer + * separated by an underscore. eg beats_state + */ +export const monitoredClustersQuery = ({ timeRange, timeout }: QueryOptions) => { + if (!timeRange) throw new Error('monitoredClustersQuery: missing timeRange parameter'); + + return { + timeout: `${timeout}s`, + query: { + bool: { + filter: [ + { + range: { + timestamp: { + gte: timeRange.min, + lte: timeRange.max, + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: clusterUuidTerm, + aggs: { + ...beatsAggregations, + cluster: clusterAggregation, + elasticsearch: esAggregation, + kibana: kibanaAggregation, + logstash: logstashAggregation, + }, + }, + }, + }; +}; + +/** + * some metricset documents use a stable ID to maintain a single occurence of + * the documents in the index. we query those metricsets separately without + * a time range filter + */ +export const persistentMetricsetsQuery = ({ timeout }: QueryOptions) => { + const metricsetsAggregations = { + elasticsearch: { + terms: { + field: 'source_node.uuid', + size: MAX_BUCKET_SIZE, + }, + aggs: { + shard: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'shards', + }, + }, + { + term: { + 'metricset.name': 'shard', + }, + }, + ], + }, + }, + }), + }, + }, + + logstash: { + terms: { + field: 'logstash_state.pipeline.id', + size: MAX_BUCKET_SIZE, + }, + aggs: { + node: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'logstash_state', + }, + }, + { + term: { + 'metricset.name': 'node', + }, + }, + ], + }, + }, + }), + }, + }, + }; + + return { + timeout: `${timeout}s`, + aggs: { + clusters: { + terms: clusterUuidTerm, + aggs: metricsetsAggregations, + }, + }, + }; +}; + +export const enterpriseSearchQuery = ({ timeRange, timeout }: QueryOptions) => { + if (!timeRange) throw new Error('enterpriseSearchQuery: missing timeRange parameter'); + + const timestampField = '@timestamp'; + return { + timeout: `${timeout}s`, + query: { + bool: { + filter: [ + { + range: { + [timestampField]: { + gte: timeRange.min, + lte: timeRange.max, + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'enterprisesearch.cluster_uuid', + size: MAX_BUCKET_SIZE, + }, + aggs: { + enterpriseSearch: { + terms: { + field: 'agent.id', + }, + aggs: { + health: lastSeenByIndex( + { + filter: { + bool: { + should: [ + { + term: { + 'metricset.name': 'health', + }, + }, + ], + }, + }, + }, + timestampField + ), + + stats: lastSeenByIndex( + { + filter: { + bool: { + should: [ + { + term: { + 'metricset.name': 'stats', + }, + }, + ], + }, + }, + }, + timestampField + ), + }, + }, + }, + }, + }, + }; +}; + +const clusterUuidTerm = { field: 'cluster_uuid', missing: 'standalone', size: 100 }; + +const lastSeenByIndex = (aggregation: { filter: any }, timestampField = 'timestamp') => { + return { + ...aggregation, + aggs: { + by_index: { + terms: { + field: '_index', + size: MAX_BUCKET_SIZE, + }, + aggs: { + last_seen: { + max: { + field: timestampField, + }, + }, + }, + }, + }, + }; +}; + +const clusterAggregation = { + terms: { + field: 'cluster_uuid', + size: MAX_BUCKET_SIZE, + }, + aggs: { + cluster_stats: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + term: { + 'metricset.name': 'cluster_stats', + }, + }, + ], + }, + }, + }), + + ccr: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'ccr_stats', + }, + }, + { + term: { + 'metricset.name': 'ccr', + }, + }, + ], + }, + }, + }), + + index: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'index_stats', + }, + }, + { + term: { + 'metricset.name': 'index', + }, + }, + ], + }, + }, + }), + + index_summary: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'indices_stats', + }, + }, + { + term: { + 'metricset.name': 'index_summary', + }, + }, + ], + }, + }, + }), + + index_recovery: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'index_recovery', + }, + }, + { + term: { + 'metricset.name': 'index_recovery', + }, + }, + ], + }, + }, + }), + }, +}; + +// ignore the enrich metricset since it's not used by stack monitoring +const esAggregation = { + terms: { + field: 'node_stats.node_id', + size: MAX_BUCKET_SIZE, + }, + aggs: { + node_stats: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'node_stats', + }, + }, + { + term: { + 'metricset.name': 'node_stats', + }, + }, + ], + }, + }, + }), + }, +}; + +const kibanaAggregation = { + terms: { + field: 'kibana_stats.kibana.uuid', + size: MAX_BUCKET_SIZE, + }, + aggs: { + stats: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'kibana_stats', + }, + }, + { + term: { + 'metricset.name': 'stats', + }, + }, + ], + }, + }, + }), + }, +}; + +const logstashAggregation = { + terms: { + field: 'logstash_stats.logstash.uuid', + size: MAX_BUCKET_SIZE, + }, + aggs: { + node_stats: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'logstash_stats', + }, + }, + { + term: { + 'metricset.name': 'node_stats', + }, + }, + ], + }, + }, + }), + }, +}; + +const beatsAggregations = { + beats_stats: { + multi_terms: { + size: MAX_BUCKET_SIZE, + terms: [ + { + field: 'beats_stats.beat.type', + }, + { + field: 'beats_stats.beat.uuid', + }, + ], + }, + aggs: { + stats: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'beats_stats', + }, + }, + { + term: { + 'metricset.name': 'stats', + }, + }, + ], + }, + }, + }), + }, + }, + + beats_state: { + multi_terms: { + size: MAX_BUCKET_SIZE, + terms: [ + { + field: 'beats_state.beat.type', + }, + { + field: 'beats_state.beat.uuid', + }, + ], + }, + aggs: { + state: lastSeenByIndex({ + filter: { + bool: { + should: [ + { + term: { + type: 'beats_state', + }, + }, + { + term: { + 'metricset.name': 'state', + }, + }, + ], + }, + }, + }), + }, + }, +}; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/index.ts index f2a1830006174..7ec331075655b 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/index.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/index.ts @@ -13,6 +13,7 @@ export { registerV1ClusterRoutes } from './cluster'; export { registerV1ElasticsearchRoutes } from './elasticsearch'; export { registerV1ElasticsearchSettingsRoutes } from './elasticsearch_settings'; export { registerV1EnterpriseSearchRoutes } from './enterprise_search'; +export { registerV1HealthRoute } from './_health'; export { registerV1LogstashRoutes } from './logstash'; export { registerV1SetupRoutes } from './setup'; export { registerV1KibanaRoutes } from './kibana'; diff --git a/x-pack/plugins/monitoring/server/routes/index.ts b/x-pack/plugins/monitoring/server/routes/index.ts index 32f2f06188d95..ce5d87a55f1da 100644 --- a/x-pack/plugins/monitoring/server/routes/index.ts +++ b/x-pack/plugins/monitoring/server/routes/index.ts @@ -17,6 +17,7 @@ import { registerV1ElasticsearchRoutes, registerV1ElasticsearchSettingsRoutes, registerV1EnterpriseSearchRoutes, + registerV1HealthRoute, registerV1LogstashRoutes, registerV1SetupRoutes, registerV1KibanaRoutes, @@ -39,6 +40,7 @@ export function requireUIRoutes( registerV1ElasticsearchRoutes(decoratedServer); registerV1ElasticsearchSettingsRoutes(decoratedServer, npRoute); registerV1EnterpriseSearchRoutes(decoratedServer); + registerV1HealthRoute(decoratedServer); registerV1LogstashRoutes(decoratedServer); registerV1SetupRoutes(decoratedServer); registerV1KibanaRoutes(decoratedServer); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 09973e33b0dc2..74d0578d459f9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -18,6 +18,10 @@ import type { import type { PersistableFilter } from '@kbn/lens-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { + FieldFormatParams as BaseFieldFormatParams, + SerializedFieldFormat, +} from '@kbn/field-formats-plugin/common'; export const ReportViewTypes = { dist: 'data-distribution', @@ -119,13 +123,16 @@ export interface ConfigProps { series?: SeriesUrl; } +interface FormatType extends SerializedFieldFormat { + id: 'duration' | 'number' | 'bytes' | 'percent'; +} + export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile'; -type FormatType = 'duration' | 'number' | 'bytes' | 'percent'; type InputFormat = 'microseconds' | 'milliseconds' | 'seconds'; type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize' | 'humanizePrecise'; -export interface FieldFormatParams { +export interface FieldFormatParams extends BaseFieldFormatParams { inputFormat?: InputFormat; outputFormat?: OutputFormat; outputPrecision?: number; @@ -135,10 +142,7 @@ export interface FieldFormatParams { export interface FieldFormat { field: string; - format: { - id: FormatType; - params: FieldFormatParams; - }; + format: FormatType; } export interface BuilderItem { 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 6e178250c53ff..1a05e991b08ff 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 d75be330df548..8318e4b7c8e60 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 745ab2ca044ff..e88467b225e9e 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/observability/public/utils/observability_data_views/observability_data_views.ts b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts index bc88769db43bf..37661c696ea88 100644 --- a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts +++ b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts @@ -65,15 +65,15 @@ const getAppDataViewId = (app: AppDataType, indices: string) => { return `${dataViewList?.[app] ?? app}_${postfix}`; }; -export function isParamsSame(param1: IFieldFormat['_params'], param2: FieldFormatParams) { +export function isParamsSame(param1: IFieldFormat['_params'], param2?: FieldFormatParams) { const isSame = param1?.inputFormat === param2?.inputFormat && param1?.outputFormat === param2?.outputFormat && param1?.useShortSuffix === param2?.useShortSuffix && param1?.showSuffix === param2?.showSuffix; - if (param2.outputPrecision !== undefined) { - return param2?.outputPrecision === param1?.outputPrecision && isSame; + if (param2?.outputPrecision !== undefined) { + return param2.outputPrecision === param1?.outputPrecision && isSame; } return isSame; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts deleted file mode 100644 index 535d0fc0e58c6..0000000000000 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CloudEcs } from '../../../ecs/cloud'; -import { HostEcs, OsEcs } from '../../../ecs/host'; -import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../common'; - -export enum HostPolicyResponseActionStatus { - success = 'success', - failure = 'failure', - warning = 'warning', -} - -export enum HostsFields { - lastSeen = 'lastSeen', - hostName = 'hostName', -} - -export interface EndpointFields { - endpointPolicy?: Maybe; - sensorVersion?: Maybe; - policyStatus?: Maybe; -} - -export interface HostItem { - _id?: Maybe; - cloud?: Maybe; - endpoint?: Maybe; - host?: Maybe; - lastSeen?: Maybe; -} - -export interface HostValue { - value: number; - value_as_string: string; -} - -export interface HostBucketItem { - key: string; - doc_count: number; - timestamp: HostValue; -} - -export interface HostBuckets { - buckets: HostBucketItem[]; -} - -export interface HostOsHitsItem { - hits: { - total: TotalValue | number; - max_score: number | null; - hits: Array<{ - _source: { host: { os: Maybe } }; - sort?: [number]; - _index?: string; - _type?: string; - _id?: string; - _score?: number | null; - }>; - }; -} - -export interface HostAggEsItem { - cloud_instance_id?: HostBuckets; - cloud_machine_type?: HostBuckets; - cloud_provider?: HostBuckets; - cloud_region?: HostBuckets; - firstSeen?: HostValue; - host_architecture?: HostBuckets; - host_id?: HostBuckets; - host_ip?: HostBuckets; - host_mac?: HostBuckets; - host_name?: HostBuckets; - host_os_name?: HostBuckets; - host_os_version?: HostBuckets; - host_type?: HostBuckets; - key?: string; - lastSeen?: HostValue; - os?: HostOsHitsItem; -} - -export interface HostEsData extends SearchHit { - sort: string[]; - aggregations: { - host_count: { - value: number; - }; - host_data: { - buckets: HostAggEsItem[]; - }; - }; -} - -export interface HostAggEsData extends SearchHit { - sort: string[]; - aggregations: HostAggEsItem; -} - -export interface HostHit extends Hit { - _source: { - '@timestamp'?: string; - host: HostEcs; - }; - cursor?: string; - firstSeen?: string; - sort?: StringOrNumber[]; -} - -export type HostHits = Hits; diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx index 178bbe3536834..dc54be07f49eb 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx @@ -226,6 +226,7 @@ const ViewResultsInLensActionComponent: React.FC = ({ const { visibleRowIndex } = actionProps as EuiDataGridCellValueElementProps & { visibleRowIndex: number; }; - const eventId = data[visibleRowIndex]._id; + const eventId = data[visibleRowIndex]?._id; return addToTimeline({ query: ['_id', eventId], isIcon: true }); }, diff --git a/x-pack/plugins/reporting/common/errors/index.ts b/x-pack/plugins/reporting/common/errors/index.ts index d2c5f0181df86..0876b84312053 100644 --- a/x-pack/plugins/reporting/common/errors/index.ts +++ b/x-pack/plugins/reporting/common/errors/index.ts @@ -33,6 +33,16 @@ export abstract class ReportingError extends Error { } } +/** + * While validating the page layout parameters for a screenshot type report job + */ +export class InvalidLayoutParametersError extends ReportingError { + static code = 'invalid_layout_parameters_error' as const; + public get code() { + return InvalidLayoutParametersError.code; + } +} + /** * While performing some reporting action, like fetching data from ES, our * access token expired. diff --git a/x-pack/plugins/reporting/common/errors/map_to_reporting_error.test.ts b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.test.ts index f846bb2c7bb91..3d5f38e4d9b89 100644 --- a/x-pack/plugins/reporting/common/errors/map_to_reporting_error.test.ts +++ b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.test.ts @@ -12,6 +12,7 @@ import { BrowserCouldNotLaunchError, BrowserUnexpectedlyClosedError, BrowserScreenshotError, + InvalidLayoutParametersError, } from '.'; describe('mapToReportingError', () => { @@ -22,6 +23,9 @@ describe('mapToReportingError', () => { }); test('Screenshotting error', () => { + expect(mapToReportingError(new errors.InvalidLayoutParametersError())).toBeInstanceOf( + InvalidLayoutParametersError + ); expect(mapToReportingError(new errors.BrowserClosedUnexpectedly())).toBeInstanceOf( BrowserUnexpectedlyClosedError ); diff --git a/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts index 1244737deee2e..3a1c5d0987b05 100644 --- a/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts +++ b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts @@ -14,13 +14,25 @@ import { BrowserScreenshotError, PdfWorkerOutOfMemoryError, VisualReportingSoftDisabledError, + InvalidLayoutParametersError, } from '.'; +/** + * Map an error object from the Screenshotting plugin into an error type of the Reporting domain. + * + * NOTE: each type of ReportingError code must be referenced in each applicable `errorCodesSchema*` object in + * x-pack/plugins/reporting/server/usage/schema.ts + * + * @param {unknown} error - a kind of error object + * @returns {ReportingError} - the converted error object + */ export function mapToReportingError(error: unknown): ReportingError { if (error instanceof ReportingError) { return error; } switch (true) { + case error instanceof errors.InvalidLayoutParametersError: + return new InvalidLayoutParametersError((error as Error).message); case error instanceof errors.BrowserClosedUnexpectedly: return new BrowserUnexpectedlyClosedError((error as Error).message); case error instanceof errors.FailedToCaptureScreenshot: diff --git a/x-pack/plugins/reporting/common/test/fixtures.ts b/x-pack/plugins/reporting/common/test/fixtures.ts index f3ad13e7eb5a8..1a78e52199534 100644 --- a/x-pack/plugins/reporting/common/test/fixtures.ts +++ b/x-pack/plugins/reporting/common/test/fixtures.ts @@ -8,7 +8,7 @@ import type { ReportApiJSON } from '../types'; import type { ReportMock } from './types'; -const buildMockReport = (baseObj: ReportMock) => ({ +const buildMockReport = (baseObj: ReportMock): ReportApiJSON => ({ index: '.reporting-2020.04.12', migration_version: '7.15.0', max_attempts: 1, 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 935f3e297b2cb..ab6a5109a1066 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 d3075d4e5a906..6f575652450c1 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 ba2c32de49f64..ef27989a6d420 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 f7b71d78de8bd..44389e164472a 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/reporting/server/export_types/common/generate_png.ts b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts index aa7cad00a8faf..328591a3056c3 100644 --- a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts @@ -9,7 +9,6 @@ import apm from 'elastic-apm-node'; import type { Logger } from '@kbn/core/server'; import * as Rx from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; -import { LayoutTypes } from '@kbn/screenshotting-plugin/common'; import type { ReportingCore } from '../..'; import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import type { PngMetrics } from '../../../common/types'; @@ -38,10 +37,7 @@ export function generatePngObservable( .getScreenshots({ ...options, format: 'png', - layout: { - id: LayoutTypes.PRESERVE_LAYOUT, - ...options.layout, - }, + layout: { id: 'preserve_layout', ...options.layout }, }) .pipe( tap(({ metrics }) => { diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 53497e2eeaea3..9daa9ea75bebe 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -10,8 +10,8 @@ import * as Rx from 'rxjs'; import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; -import { PngScreenshotOptions, RunTaskFn, RunTaskFnFactory } from '../../../types'; -import { decryptJobHeaders, getFullUrls, generatePngObservable } from '../../common'; +import { RunTaskFn, RunTaskFnFactory } from '../../../types'; +import { decryptJobHeaders, generatePngObservable, getFullUrls } from '../../common'; import { TaskPayloadPNG } from '../types'; export const runTaskFnFactory: RunTaskFnFactory> = @@ -39,10 +39,8 @@ export const runTaskFnFactory: RunTaskFnFactory> = browserTimezone: job.browserTimezone, layout: { ...job.layout, - // TODO: We do not do a runtime check for supported layout id types for now. But technically - // we should. - id: job.layout?.id, - } as PngScreenshotOptions['layout'], + id: 'preserve_layout', + }, }); }), tap(({ buffer }) => stream.write(buffer)), diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts index bd32bdad4e605..54767981b95b5 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts @@ -10,7 +10,7 @@ import * as Rx from 'rxjs'; import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; -import { PngScreenshotOptions, RunTaskFn, RunTaskFnFactory } from '../../types'; +import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders, generatePngObservable } from '../common'; import { getFullRedirectAppUrl } from '../common/v2/get_full_redirect_app_url'; import { TaskPayloadPNGV2 } from './types'; @@ -38,12 +38,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = return generatePngObservable(reporting, jobLogger, { headers, browserTimezone: job.browserTimezone, - layout: { - ...job.layout, - // TODO: We do not do a runtime check for supported layout id types for now. But technically - // we should. - id: job.layout?.id, - } as PngScreenshotOptions['layout'], + layout: { ...job.layout, id: 'preserve_layout' }, urls: [[url, locatorParams]], }); }), diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 6341d0a253e50..c42d75b6533f2 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -10,8 +10,8 @@ import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; -import { PdfScreenshotOptions, RunTaskFn, RunTaskFnFactory } from '../../../types'; -import { decryptJobHeaders, getFullUrls, getCustomLogo } from '../../common'; +import { RunTaskFn, RunTaskFnFactory } from '../../../types'; +import { decryptJobHeaders, getCustomLogo, getFullUrls } from '../../common'; import { generatePdfObservable } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; @@ -43,12 +43,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = urls, browserTimezone, headers, - layout: { - ...layout, - // TODO: We do not do a runtime check for supported layout id types for now. But technically - // we should. - id: layout?.id, - } as PdfScreenshotOptions['layout'], + layout, }); }), tap(({ buffer }) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts index b7e6a25f1848f..59da386c528b0 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts @@ -9,7 +9,6 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; -import type { PdfScreenshotOptions } from '../../types'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders, getCustomLogo } from '../common'; @@ -41,12 +40,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = logo, browserTimezone, headers, - layout: { - ...layout, - // TODO: We do not do a runtime check for supported layout id types for now. But technically - // we should. - id: layout?.id, - } as PdfScreenshotOptions['layout'], + layout, }); }), tap(({ buffer }) => { diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index 937fb0217f4bf..ff3d28fc19a4e 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -39,6 +39,9 @@ Object { "browser_unexpectedly_closed_error": Object { "type": "long", }, + "invalid_layout_parameters_error": Object { + "type": "long", + }, "kibana_shutting_down_error": Object { "type": "long", }, @@ -143,6 +146,9 @@ Object { "browser_unexpectedly_closed_error": Object { "type": "long", }, + "invalid_layout_parameters_error": Object { + "type": "long", + }, "kibana_shutting_down_error": Object { "type": "long", }, @@ -413,6 +419,9 @@ Object { "browser_unexpectedly_closed_error": Object { "type": "long", }, + "invalid_layout_parameters_error": Object { + "type": "long", + }, "kibana_shutting_down_error": Object { "type": "long", }, @@ -517,6 +526,9 @@ Object { "browser_unexpectedly_closed_error": Object { "type": "long", }, + "invalid_layout_parameters_error": Object { + "type": "long", + }, "kibana_shutting_down_error": Object { "type": "long", }, @@ -803,6 +815,9 @@ Object { "browser_unexpectedly_closed_error": Object { "type": "long", }, + "invalid_layout_parameters_error": Object { + "type": "long", + }, "kibana_shutting_down_error": Object { "type": "long", }, @@ -935,6 +950,9 @@ Object { "browser_unexpectedly_closed_error": Object { "type": "long", }, + "invalid_layout_parameters_error": Object { + "type": "long", + }, "kibana_shutting_down_error": Object { "type": "long", }, @@ -1540,6 +1558,9 @@ Object { "browser_unexpectedly_closed_error": Object { "type": "long", }, + "invalid_layout_parameters_error": Object { + "type": "long", + }, "kibana_shutting_down_error": Object { "type": "long", }, @@ -1672,6 +1693,9 @@ Object { "browser_unexpectedly_closed_error": Object { "type": "long", }, + "invalid_layout_parameters_error": Object { + "type": "long", + }, "kibana_shutting_down_error": Object { "type": "long", }, diff --git a/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts index f99c81ea39e29..dbbc219fa8733 100644 --- a/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts @@ -332,6 +332,7 @@ test('Incorporate error code stats', () => { browser_unexpectedly_closed_error: 8, browser_screenshot_error: 27, visual_reporting_soft_disabled_error: 1, + invalid_layout_parameters_error: 0, }, }, printable_pdf_v2: { @@ -351,6 +352,7 @@ test('Incorporate error code stats', () => { browser_unexpectedly_closed_error: 8, browser_screenshot_error: 27, visual_reporting_soft_disabled_error: 1, + invalid_layout_parameters_error: 0, }, }, csv_searchsource_immediate: { @@ -377,6 +379,7 @@ test('Incorporate error code stats', () => { "browser_could_not_launch_error": 2, "browser_screenshot_error": 27, "browser_unexpectedly_closed_error": 8, + "invalid_layout_parameters_error": 0, "kibana_shutting_down_error": 1, "queue_timeout_error": 1, "unknown_error": 0, @@ -389,6 +392,7 @@ test('Incorporate error code stats', () => { "browser_could_not_launch_error": 2, "browser_screenshot_error": 27, "browser_unexpectedly_closed_error": 8, + "invalid_layout_parameters_error": 0, "kibana_shutting_down_error": 1, "pdf_worker_out_of_memory_error": 99, "queue_timeout_error": 1, diff --git a/x-pack/plugins/reporting/server/usage/schema.test.ts b/x-pack/plugins/reporting/server/usage/schema.test.ts index f877b6251378e..1832417c3ea67 100644 --- a/x-pack/plugins/reporting/server/usage/schema.test.ts +++ b/x-pack/plugins/reporting/server/usage/schema.test.ts @@ -36,6 +36,7 @@ describe('Reporting telemetry schema', () => { "PNG.error_codes.browser_could_not_launch_error.type": "long", "PNG.error_codes.browser_screenshot_error.type": "long", "PNG.error_codes.browser_unexpectedly_closed_error.type": "long", + "PNG.error_codes.invalid_layout_parameters_error.type": "long", "PNG.error_codes.kibana_shutting_down_error.type": "long", "PNG.error_codes.queue_timeout_error.type": "long", "PNG.error_codes.unknown_error.type": "long", @@ -66,6 +67,7 @@ describe('Reporting telemetry schema', () => { "PNGV2.error_codes.browser_could_not_launch_error.type": "long", "PNGV2.error_codes.browser_screenshot_error.type": "long", "PNGV2.error_codes.browser_unexpectedly_closed_error.type": "long", + "PNGV2.error_codes.invalid_layout_parameters_error.type": "long", "PNGV2.error_codes.kibana_shutting_down_error.type": "long", "PNGV2.error_codes.queue_timeout_error.type": "long", "PNGV2.error_codes.unknown_error.type": "long", @@ -143,6 +145,7 @@ describe('Reporting telemetry schema', () => { "last7Days.PNG.error_codes.browser_could_not_launch_error.type": "long", "last7Days.PNG.error_codes.browser_screenshot_error.type": "long", "last7Days.PNG.error_codes.browser_unexpectedly_closed_error.type": "long", + "last7Days.PNG.error_codes.invalid_layout_parameters_error.type": "long", "last7Days.PNG.error_codes.kibana_shutting_down_error.type": "long", "last7Days.PNG.error_codes.queue_timeout_error.type": "long", "last7Days.PNG.error_codes.unknown_error.type": "long", @@ -173,6 +176,7 @@ describe('Reporting telemetry schema', () => { "last7Days.PNGV2.error_codes.browser_could_not_launch_error.type": "long", "last7Days.PNGV2.error_codes.browser_screenshot_error.type": "long", "last7Days.PNGV2.error_codes.browser_unexpectedly_closed_error.type": "long", + "last7Days.PNGV2.error_codes.invalid_layout_parameters_error.type": "long", "last7Days.PNGV2.error_codes.kibana_shutting_down_error.type": "long", "last7Days.PNGV2.error_codes.queue_timeout_error.type": "long", "last7Days.PNGV2.error_codes.unknown_error.type": "long", @@ -255,6 +259,7 @@ describe('Reporting telemetry schema', () => { "last7Days.printable_pdf.error_codes.browser_could_not_launch_error.type": "long", "last7Days.printable_pdf.error_codes.browser_screenshot_error.type": "long", "last7Days.printable_pdf.error_codes.browser_unexpectedly_closed_error.type": "long", + "last7Days.printable_pdf.error_codes.invalid_layout_parameters_error.type": "long", "last7Days.printable_pdf.error_codes.kibana_shutting_down_error.type": "long", "last7Days.printable_pdf.error_codes.pdf_worker_out_of_memory_error.type": "long", "last7Days.printable_pdf.error_codes.queue_timeout_error.type": "long", @@ -293,6 +298,7 @@ describe('Reporting telemetry schema', () => { "last7Days.printable_pdf_v2.error_codes.browser_could_not_launch_error.type": "long", "last7Days.printable_pdf_v2.error_codes.browser_screenshot_error.type": "long", "last7Days.printable_pdf_v2.error_codes.browser_unexpectedly_closed_error.type": "long", + "last7Days.printable_pdf_v2.error_codes.invalid_layout_parameters_error.type": "long", "last7Days.printable_pdf_v2.error_codes.kibana_shutting_down_error.type": "long", "last7Days.printable_pdf_v2.error_codes.pdf_worker_out_of_memory_error.type": "long", "last7Days.printable_pdf_v2.error_codes.queue_timeout_error.type": "long", @@ -463,6 +469,7 @@ describe('Reporting telemetry schema', () => { "printable_pdf.error_codes.browser_could_not_launch_error.type": "long", "printable_pdf.error_codes.browser_screenshot_error.type": "long", "printable_pdf.error_codes.browser_unexpectedly_closed_error.type": "long", + "printable_pdf.error_codes.invalid_layout_parameters_error.type": "long", "printable_pdf.error_codes.kibana_shutting_down_error.type": "long", "printable_pdf.error_codes.pdf_worker_out_of_memory_error.type": "long", "printable_pdf.error_codes.queue_timeout_error.type": "long", @@ -501,6 +508,7 @@ describe('Reporting telemetry schema', () => { "printable_pdf_v2.error_codes.browser_could_not_launch_error.type": "long", "printable_pdf_v2.error_codes.browser_screenshot_error.type": "long", "printable_pdf_v2.error_codes.browser_unexpectedly_closed_error.type": "long", + "printable_pdf_v2.error_codes.invalid_layout_parameters_error.type": "long", "printable_pdf_v2.error_codes.kibana_shutting_down_error.type": "long", "printable_pdf_v2.error_codes.pdf_worker_out_of_memory_error.type": "long", "printable_pdf_v2.error_codes.queue_timeout_error.type": "long", diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index f89d89d35503d..f2a3b74f5eea6 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -89,6 +89,7 @@ const errorCodesSchemaPng: MakeSchemaFrom = { browser_unexpectedly_closed_error: { type: 'long' }, browser_screenshot_error: { type: 'long' }, visual_reporting_soft_disabled_error: { type: 'long' }, + invalid_layout_parameters_error: { type: 'long' }, }; const errorCodesSchemaPdf: MakeSchemaFrom = { pdf_worker_out_of_memory_error: { type: 'long' }, @@ -100,6 +101,7 @@ const errorCodesSchemaPdf: MakeSchemaFrom = { diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index 9d7e08932cc1c..6974b2e512eaf 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -198,6 +198,7 @@ export interface ErrorCodeStats { authentication_expired_error: number | null; queue_timeout_error: number | null; unknown_error: number | null; + invalid_layout_parameters_error: number | null; pdf_worker_out_of_memory_error: number | null; browser_could_not_launch_error: number | null; browser_unexpectedly_closed_error: number | null; diff --git a/x-pack/plugins/screenshotting/common/errors.ts b/x-pack/plugins/screenshotting/common/errors.ts index 5284e808c7993..06823ef812922 100644 --- a/x-pack/plugins/screenshotting/common/errors.ts +++ b/x-pack/plugins/screenshotting/common/errors.ts @@ -6,6 +6,8 @@ */ /* eslint-disable max-classes-per-file */ +export class InvalidLayoutParametersError extends Error {} + export class PdfWorkerOutOfMemoryError extends Error {} export class FailedToSpawnBrowserError extends Error {} diff --git a/x-pack/plugins/screenshotting/common/index.ts b/x-pack/plugins/screenshotting/common/index.ts index 7570477a1c1c9..3e36d061cba49 100644 --- a/x-pack/plugins/screenshotting/common/index.ts +++ b/x-pack/plugins/screenshotting/common/index.ts @@ -5,14 +5,13 @@ * 2.0. */ -export type { LayoutParams } from './layout'; -export { LayoutTypes } from './layout'; -import * as errors from './errors'; -export { errors }; export { SCREENSHOTTING_APP_ID, SCREENSHOTTING_EXPRESSION, SCREENSHOTTING_EXPRESSION_INPUT, } from './expression'; +export type { LayoutParams, LayoutType } from './layout'; +export { errors }; +import * as errors from './errors'; export const PLUGIN_ID = 'screenshotting'; diff --git a/x-pack/plugins/screenshotting/common/layout.ts b/x-pack/plugins/screenshotting/common/layout.ts index 4362375b564a5..2f53c928d92b1 100644 --- a/x-pack/plugins/screenshotting/common/layout.ts +++ b/x-pack/plugins/screenshotting/common/layout.ts @@ -40,7 +40,7 @@ export interface LayoutSelectorDictionary { /** * Screenshot layout parameters. */ -export type LayoutParams = Ensure< +export type LayoutParams = Ensure< { /** * Unique layout name. @@ -68,8 +68,4 @@ export type LayoutParams = Ensure< /** * Supported layout types. */ -export enum LayoutTypes { - PRESERVE_LAYOUT = 'preserve_layout', - PRINT = 'print', - CANVAS = 'canvas', -} +export type LayoutType = 'preserve_layout' | 'print' | 'canvas'; diff --git a/x-pack/plugins/screenshotting/public/index.ts b/x-pack/plugins/screenshotting/public/index.ts index 29997c1b0dc2b..0ef7052004bb4 100755 --- a/x-pack/plugins/screenshotting/public/index.ts +++ b/x-pack/plugins/screenshotting/public/index.ts @@ -12,4 +12,3 @@ export function plugin() { } export type { LayoutParams } from '../common'; -export { LayoutTypes } from '../common'; diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts index 716b2bd46352f..8a4571c128f9c 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts @@ -9,11 +9,9 @@ // we should get rid of this lib. import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js'; -import type { Values } from '@kbn/utility-types'; -import { groupBy } from 'lodash'; import type { PackageInfo } from '@kbn/core/server'; -import type { LayoutParams } from '../../../common'; -import { LayoutTypes } from '../../../common'; +import { groupBy } from 'lodash'; +import type { LayoutParams, LayoutType } from '../../../common'; import type { Layout } from '../../layouts'; import type { CaptureMetrics, CaptureOptions, CaptureResult } from '../../screenshots'; import { EventLogger, Transactions } from '../../screenshots/event_logger'; @@ -25,9 +23,7 @@ import { pngsToPdf } from './pdf_maker'; * => When creating a PDF intended for print multiple PNGs will be spread out across pages * => When creating a PDF from a Canvas workpad, each page in the workpad will be placed on a separate page */ -export type PdfLayoutParams = LayoutParams< - Values> ->; +export type PdfLayoutParams = LayoutParams; /** * Options that should be provided to a PDF screenshot request. @@ -105,7 +101,7 @@ export async function toPdf( ): Promise { let buffer: Buffer; let pages: number; - const shouldConvertPngsToPdf = layout.id !== LayoutTypes.PRINT; + const shouldConvertPngsToPdf = layout.id !== 'print'; if (shouldConvertPngsToPdf) { const timeRange = getTimeRange(results); try { diff --git a/x-pack/plugins/screenshotting/server/formats/png.ts b/x-pack/plugins/screenshotting/server/formats/png.ts index c3338f5a9194f..caa752d2f0a83 100644 --- a/x-pack/plugins/screenshotting/server/formats/png.ts +++ b/x-pack/plugins/screenshotting/server/formats/png.ts @@ -7,12 +7,11 @@ import type { CaptureResult, CaptureOptions } from '../screenshots'; import type { LayoutParams } from '../../common'; -import { LayoutTypes } from '../../common'; /** * The layout parameters that are accepted by PNG screenshots */ -export type PngLayoutParams = LayoutParams; +export type PngLayoutParams = LayoutParams<'preserve_layout'>; /** * Options that should be provided to a screenshot PNG request diff --git a/x-pack/plugins/screenshotting/server/layouts/base_layout.ts b/x-pack/plugins/screenshotting/server/layouts/base_layout.ts index e713c4c3cdcf2..31bff5bf47384 100644 --- a/x-pack/plugins/screenshotting/server/layouts/base_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/base_layout.ts @@ -6,7 +6,7 @@ */ import type { CustomPageSize, PredefinedPageSize } from 'pdfmake/interfaces'; -import type { Size } from '../../common/layout'; +import type { LayoutType, Size } from '../../common/layout'; export interface ViewZoomWidthHeight { zoom: number; @@ -29,14 +29,14 @@ export interface PageSizeParams { } export abstract class BaseLayout { - public id: string = ''; + public id: LayoutType; public groupCount: number = 0; public hasHeader: boolean = true; public hasFooter: boolean = true; public useReportingBranding: boolean = true; - constructor(id: string) { + constructor(id: LayoutType) { this.id = id; } diff --git a/x-pack/plugins/screenshotting/server/layouts/canvas_layout.ts b/x-pack/plugins/screenshotting/server/layouts/canvas_layout.ts index d164f8c7e91e2..9312561b2c5f7 100644 --- a/x-pack/plugins/screenshotting/server/layouts/canvas_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/canvas_layout.ts @@ -6,7 +6,6 @@ */ import type { LayoutSelectorDictionary, Size } from '../../common/layout'; -import { LayoutTypes } from '../../common'; import { DEFAULT_SELECTORS } from '.'; import type { Layout } from '.'; import { BaseLayout } from './base_layout'; @@ -33,7 +32,7 @@ export class CanvasLayout extends BaseLayout implements Layout { public useReportingBranding: boolean = false; constructor(size: Size) { - super(LayoutTypes.CANVAS); + super('canvas'); this.height = size.height; this.width = size.width; this.scaledHeight = size.height * ZOOM; diff --git a/x-pack/plugins/screenshotting/server/layouts/create_layout.ts b/x-pack/plugins/screenshotting/server/layouts/create_layout.ts index fa4b3a40e2c79..ec4a4c6fa8347 100644 --- a/x-pack/plugins/screenshotting/server/layouts/create_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/create_layout.ts @@ -5,28 +5,47 @@ * 2.0. */ -import { map as mapRecord } from 'fp-ts/lib/Record'; -import type { LayoutParams } from '../../common/layout'; -import { LayoutTypes } from '../../common'; +import { InvalidLayoutParametersError } from '../../common/errors'; +import type { LayoutParams, LayoutType } from '../../common/layout'; import type { Layout } from '.'; import { CanvasLayout } from './canvas_layout'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; +// utility for validating the layout type from user's job params +const LAYOUTS: LayoutType[] = ['canvas', 'print', 'preserve_layout']; + /** - * We naively round all numeric values in the object, this will break screenshotting - * if ever a have a non-number set as a value, but this points to an issue - * in the code responsible for creating the dimensions object. + * Layout dimensions must be sanitized as they are passed in the args that spawn the + * Chromium process. Width and height must be int32 value. + * */ -const roundNumbers = mapRecord(Math.round); +const sanitizeLayout = (dimensions: { width: number; height: number }) => { + const { width, height } = dimensions; + if (isNaN(width) || isNaN(height)) { + throw new InvalidLayoutParametersError(`Invalid layout width or height`); + } + return { + width: Math.round(width), + height: Math.round(height), + }; +}; export function createLayout({ id, dimensions, selectors, ...config }: LayoutParams): Layout { - if (dimensions && id === LayoutTypes.PRESERVE_LAYOUT) { - return new PreserveLayout(roundNumbers(dimensions), selectors); + const layoutId = id ?? 'print'; + + if (!LAYOUTS.includes(layoutId)) { + throw new InvalidLayoutParametersError(`Invalid layout type`); } - if (dimensions && id === LayoutTypes.CANVAS) { - return new CanvasLayout(roundNumbers(dimensions)); + if (dimensions) { + if (layoutId === 'preserve_layout') { + return new PreserveLayout(sanitizeLayout(dimensions), selectors); + } + + if (layoutId === 'canvas') { + return new CanvasLayout(sanitizeLayout(dimensions)); + } } // layoutParams is optional as PrintLayout doesn't use it diff --git a/x-pack/plugins/screenshotting/server/layouts/mock.ts b/x-pack/plugins/screenshotting/server/layouts/mock.ts index d5395c5db6f82..2e1fb083a714e 100644 --- a/x-pack/plugins/screenshotting/server/layouts/mock.ts +++ b/x-pack/plugins/screenshotting/server/layouts/mock.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { LayoutTypes } from '../../common'; import { createLayout, Layout } from '.'; export function createMockLayout(): Layout { const layout = createLayout({ - id: LayoutTypes.PRESERVE_LAYOUT, + id: 'preserve_layout', dimensions: { height: 100, width: 100 }, zoom: 1, }) as Layout; diff --git a/x-pack/plugins/screenshotting/server/layouts/preserve_layout.ts b/x-pack/plugins/screenshotting/server/layouts/preserve_layout.ts index f265920675f85..e2e7eca690d53 100644 --- a/x-pack/plugins/screenshotting/server/layouts/preserve_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/preserve_layout.ts @@ -7,7 +7,6 @@ import path from 'path'; import type { CustomPageSize } from 'pdfmake/interfaces'; import type { LayoutSelectorDictionary, Size } from '../../common/layout'; -import { LayoutTypes } from '../../common'; import { DEFAULT_SELECTORS } from '.'; import type { Layout } from '.'; import { BaseLayout } from './base_layout'; @@ -25,7 +24,7 @@ export class PreserveLayout extends BaseLayout implements Layout { private readonly scaledWidth: number; constructor(size: Size, selectors?: Partial) { - super(LayoutTypes.PRESERVE_LAYOUT); + super('preserve_layout'); this.height = size.height; this.width = size.width; this.scaledHeight = size.height * ZOOM; diff --git a/x-pack/plugins/screenshotting/server/layouts/print_layout.ts b/x-pack/plugins/screenshotting/server/layouts/print_layout.ts index e9beb2821b11b..fa6e280764851 100644 --- a/x-pack/plugins/screenshotting/server/layouts/print_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/print_layout.ts @@ -8,7 +8,6 @@ import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import type { Layout } from '.'; import { DEFAULT_SELECTORS } from '.'; -import { LayoutTypes } from '../../common'; import type { LayoutParams, LayoutSelectorDictionary } from '../../common/layout'; import { DEFAULT_VIEWPORT } from '../browsers'; import { BaseLayout } from './base_layout'; @@ -23,7 +22,7 @@ export class PrintLayout extends BaseLayout implements Layout { private zoom: number; constructor({ zoom = 1 }: Pick) { - super(LayoutTypes.PRINT); + super('print'); this.zoom = zoom; } diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index 3766104cd9e12..4bddf4f95b3aa 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -8,7 +8,7 @@ import type { Headers } from '@kbn/core/server'; import { defer, forkJoin, Observable, throwError } from 'rxjs'; import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; -import { errors, LayoutTypes } from '../../common'; +import { errors } from '../../common'; import { Context, DEFAULT_VIEWPORT, @@ -242,10 +242,7 @@ export class ScreenshotObservableHandler { } private shouldCapturePdf(): boolean { - return ( - this.layout.id === LayoutTypes.PRINT && - (this.options as PdfScreenshotOptions).format === 'pdf' - ); + return this.layout.id === 'print' && (this.options as PdfScreenshotOptions).format === 'pdf'; } public getScreenshots() { diff --git a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts index 9014e504b405b..de87e7161bda8 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { apiKeysMock } from './api_keys/api_keys.mock'; import type { InternalAuthenticationServiceStart } from './authentication_service'; diff --git a/x-pack/plugins/security/server/routes/api_keys/create.test.ts b/x-pack/plugins/security/server/routes/api_keys/create.test.ts index 22e4bb3df96d5..7236e46df7ed5 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.test.ts @@ -10,7 +10,7 @@ import Boom from '@hapi/boom'; import type { RequestHandler } from '@kbn/core/server'; import { kibanaResponseFactory } from '@kbn/core/server'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { InternalAuthenticationServiceStart } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts index 7beef71256c46..b123c5cc0be80 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -10,7 +10,7 @@ import Boom from '@hapi/boom'; import type { RequestHandler } from '@kbn/core/server'; import { kibanaResponseFactory } from '@kbn/core/server'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { InternalAuthenticationServiceStart } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 6903cc23b02f5..46a9bb729d76b 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -9,7 +9,7 @@ import { Type } from '@kbn/config-schema'; import type { RequestHandler, RouteConfig } from '@kbn/core/server'; import { kibanaResponseFactory } from '@kbn/core/server'; import { httpServerMock } from '@kbn/core/server/mocks'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index a38b53933132e..a3fb47afb0ae8 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -8,7 +8,7 @@ import { Type } from '@kbn/config-schema'; import type { RequestHandler, RouteConfig } from '@kbn/core/server'; import { httpServerMock } from '@kbn/core/server/mocks'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import type { InternalAuthenticationServiceStart } from '../../authentication'; diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts index e8ab87a504458..e5e3135a68024 100644 --- a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts @@ -8,7 +8,7 @@ import type { RequestHandler, RouteConfig } from '@kbn/core/server'; import { kibanaResponseFactory } from '@kbn/core/server'; import { httpServerMock } from '@kbn/core/server/mocks'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { RouteDefinitionParams } from '../..'; import type { CheckPrivileges } from '../../../authorization/types'; diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts index 9dc0c34ef8818..4cb5e8ffbf93d 100644 --- a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts +++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts @@ -11,7 +11,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { RequestHandler, RouteConfig } from '@kbn/core/server'; import { kibanaResponseFactory } from '@kbn/core/server'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { securityMock } from '../../mocks'; import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types'; diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index 3cda2a0ec9bc5..5241c10669dbd 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -14,7 +14,7 @@ import { loggingSystemMock, } from '@kbn/core/server/mocks'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { RouteDefinitionParams } from '.'; import { licenseMock } from '../../common/licensing/index.mock'; diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index 19f84ce461627..0ca3d759aa69f 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -12,7 +12,7 @@ import type { Headers, RequestHandler, RouteConfig } from '@kbn/core/server'; import { kibanaResponseFactory } from '@kbn/core/server'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { AuthenticationResult } from '../../authentication'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index 8a9a047aab3fd..70fa8ed5892ce 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -9,6 +9,7 @@ import { CloudEcs } from '../../../../ecs/cloud'; import { HostEcs, OsEcs } from '../../../../ecs/host'; import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../../common'; import { EndpointPendingActions, HostStatus } from '../../../../endpoint/types'; +import { CommonFields } from '../..'; export enum HostPolicyResponseActionStatus { success = 'success', @@ -63,12 +64,17 @@ export interface HostBuckets { buckets: HostBucketItem[]; } +type HostOsFields = CommonFields & + Partial<{ + [Property in keyof OsEcs as `host.os.${Property}`]: unknown[]; + }>; + export interface HostOsHitsItem { hits: { total: TotalValue | number; max_score: number | null; hits: Array<{ - _source: { host: { os: Maybe } }; + fields: HostOsFields; sort?: [number]; _index?: string; _type?: string; @@ -115,11 +121,13 @@ export interface HostAggEsData extends SearchHit { aggregations: HostAggEsItem; } +type HostFields = CommonFields & + Partial<{ + [Property in keyof HostEcs as `host.${Property}`]: unknown[]; + }>; + export interface HostHit extends Hit { - _source: { - '@timestamp'?: string; - host: HostEcs; - }; + fields: HostFields; cursor?: string; firstSeen?: string; sort?: StringOrNumber[]; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts index c4bb787ba8198..5bca04713f7b9 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts @@ -21,6 +21,7 @@ import { TotalHit, StringOrNumber, Hits, + CommonFields, } from '../../..'; export interface HostsUncommonProcessesRequestOptions extends RequestOptionsPaginated { @@ -48,16 +49,21 @@ export interface HostsUncommonProcessItem { user?: Maybe; } +type ProcessUserFields = CommonFields & + Partial<{ + [Property in keyof ProcessEcs as `process.${Property}`]: unknown[]; + }> & + Partial<{ + [Property in keyof UserEcs as `user.${Property}`]: unknown[]; + }>; + export interface HostsUncommonProcessHit extends Hit { total: TotalHit; host: Array<{ id: string[] | undefined; name: string[] | undefined; }>; - _source: { - '@timestamp': string; - process: ProcessEcs; - }; + fields: ProcessUserFields; cursor: string; sort: StringOrNumber[]; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 57a511d934879..c9d97a704589a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -256,3 +256,6 @@ export interface DocValueFieldsInput { format: string; } +export interface CommonFields { + '@timestamp'?: string[]; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/authentications/index.ts index be60776e683f4..5ce6df9c89915 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/authentications/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/authentications/index.ts @@ -19,7 +19,7 @@ import { Hit, TotalHit, } from '../../../common'; -import { RequestOptionsPaginated } from '../..'; +import { CommonFields, RequestOptionsPaginated } from '../..'; export interface UserAuthenticationsStrategyResponse extends IEsSearchResponse { edges: AuthenticationsEdges[]; @@ -71,6 +71,14 @@ export interface AuthenticationHit extends Hit { sort: StringOrNumber[]; } +type AuthenticationFields = CommonFields & + Partial<{ + [Property in keyof SourceEcs as `source.${Property}`]: unknown[]; + }> & + Partial<{ + [Property in keyof HostEcs as `host.${Property}`]: unknown[]; + }>; + export interface AuthenticationBucket { key: string; doc_count: number; @@ -83,7 +91,7 @@ export interface AuthenticationBucket { authentication: { hits: { total: TotalHit; - hits: ArrayLike; + hits: ArrayLike; }; }; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/common/index.ts index 9f3a2e94e7e13..0b338b197e9c5 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/common/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Maybe, RiskSeverity, SortField } from '../../..'; +import { CommonFields, Maybe, RiskSeverity, SortField } from '../../..'; import { HostEcs } from '../../../../ecs/host'; import { UserEcs } from '../../../../ecs/user'; @@ -64,10 +64,15 @@ export interface AllUsersAggEsItem { lastSeen?: { value_as_string: string }; } -export interface UsersDomainHitsItem { +type UserFields = CommonFields & + Partial<{ + [Property in keyof UserEcs as `user.${Property}`]: unknown[]; + }>; + +interface UsersDomainHitsItem { hits: { hits: Array<{ - fields: { user: { domain: Maybe } }; + fields: UserFields; }>; }; } diff --git a/x-pack/plugins/security_solution/cypress/screens/lists.ts b/x-pack/plugins/security_solution/cypress/screens/lists.ts index e85522bd9235c..b125bb512548b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/lists.ts +++ b/x-pack/plugins/security_solution/cypress/screens/lists.ts @@ -14,5 +14,5 @@ export const VALUE_LIST_TYPE_SELECTOR = '[data-test-subj="value-lists-form-selec export const VALUE_LIST_DELETE_BUTTON = (name: string) => `[data-test-subj="action-delete-value-list-${name}"]`; export const VALUE_LIST_FILES = '[data-test-subj*="action-delete-value-list-"]'; -export const VALUE_LIST_CLOSE_BUTTON = '[data-test-subj="value-lists-modal-close-action"]'; +export const VALUE_LIST_CLOSE_BUTTON = '[data-test-subj="value-lists-flyout-close-action"]'; export const VALUE_LIST_EXPORT_BUTTON = '[data-test-subj="action-export-value-list"]'; diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index a9abc7cc346fc..49c90c4479187 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -139,6 +139,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ defaultMessage: 'Detection & Response', }), ], + searchable: true, }, { id: SecurityPageName.detections, diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts index c6139557964a8..010ea775180a0 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -171,6 +171,10 @@ export const navTabs: SecurityNav = { }; export const securityNavGroup: SecurityNavGroup = { + [SecurityNavGroupKey.dashboards]: { + id: SecurityNavGroupKey.dashboards, + name: i18n.DASHBOARDS, + }, [SecurityNavGroupKey.detect]: { id: SecurityNavGroupKey.detect, name: i18n.DETECT, diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap index fcdd183792163..84b90a910574f 100644 --- a/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap @@ -206,7 +206,7 @@ exports[`Authentication Host Table Component rendering it renders the host authe = ({ - docValueFields, endDate, filterQuery, indexNames, @@ -59,7 +58,6 @@ const AuthenticationsHostTableComponent: React.FC = ( loading, { authentications, totalCount, pageInfo, loadPage, inspect, isInspected, refetch }, ] = useAuthentications({ - docValueFields, endDate, filterQuery, indexNames, diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.tsx b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.tsx index 312670038e96e..4a717530ddda1 100644 --- a/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.tsx @@ -28,7 +28,6 @@ import { AuthenticationsUserTableProps } from './types'; const TABLE_QUERY_ID = 'authenticationsUsersTableQuery'; const AuthenticationsUserTableComponent: React.FC = ({ - docValueFields, endDate, filterQuery, indexNames, @@ -53,7 +52,6 @@ const AuthenticationsUserTableComponent: React.FC loading, { authentications, totalCount, pageInfo, loadPage, inspect, isInspected, refetch }, ] = useAuthentications({ - docValueFields, endDate, filterQuery, indexNames, diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/authentication/helpers.tsx index cd323af0c12c1..b4fc71930d641 100644 --- a/x-pack/plugins/security_solution/public/common/components/authentication/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/authentication/helpers.tsx @@ -8,13 +8,9 @@ import { has } from 'lodash/fp'; import React from 'react'; -import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { getEmptyTagValue } from '../empty_value'; import { FormattedRelativePreferenceDate } from '../formatted_date'; import { Columns, ItemsPerRow } from '../paginated_table'; -import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; import { getRowItemDraggables } from '../tables/helpers'; import * as i18n from './translations'; @@ -77,40 +73,9 @@ export const rowItems: ItemsPerRow[] = [ const FAILURES_COLUMN: Columns = { name: i18n.FAILURES, + field: 'node.failures', truncateText: false, mobileOptions: { show: true }, - render: ({ node }) => { - const id = escapeDataProviderId(`authentications-table-${node._id}-failures-${node.failures}`); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - node.failures - ) - } - /> - ); - }, width: '8%', }; const LAST_SUCCESSFUL_TIME_COLUMN: Columns = { @@ -226,42 +191,9 @@ const HOST_COLUMN: Columns = { const SUCCESS_COLUMN: Columns = { name: i18n.SUCCESSES, + field: 'node.successes', truncateText: false, mobileOptions: { show: true }, - render: ({ node }) => { - const id = escapeDataProviderId( - `authentications-table-${node._id}-node-successes-${node.successes}` - ); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - node.successes - ) - } - /> - ); - }, width: '8%', }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 4bc47ea02dba1..1f3c3854f9284 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -26,6 +26,7 @@ export interface NavGroupTab { name: string; } export enum SecurityNavGroupKey { + dashboards = 'dashboards', detect = 'detect', explore = 'explore', investigate = 'investigate', diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap index 6936da8fbc9e9..9cbf51fe61441 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap @@ -17,6 +17,12 @@ Object { "name": "Get started", "onClick": [Function], }, + ], + "name": "", + }, + Object { + "id": "dashboards", + "items": Array [ Object { "data-href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-overview", @@ -38,7 +44,7 @@ Object { "onClick": [Function], }, ], - "name": "", + "name": "Dashboards", }, Object { "id": "detect", diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index 64f7d3c2ddfb7..c2e733edf66d5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -119,7 +119,7 @@ describe('useSecuritySolutionNavigation', () => { { wrapper: TestProviders } ); - expect(result?.current?.items?.[2].items?.[2].id).toEqual(SecurityPageName.users); + expect(result?.current?.items?.[3].items?.[2].id).toEqual(SecurityPageName.users); }); // TODO: [kubernetes] remove when no longer experimental @@ -129,7 +129,7 @@ describe('useSecuritySolutionNavigation', () => { () => useSecuritySolutionNavigation(), { wrapper: TestProviders } ); - expect(result?.current?.items?.[2].items?.[3].id).toEqual(SecurityPageName.kubernetes); + expect(result?.current?.items?.[1].items?.[2].id).toEqual(SecurityPageName.kubernetes); }); it('should omit host isolation exceptions if hook reports false', () => { @@ -160,7 +160,7 @@ describe('useSecuritySolutionNavigation', () => { { wrapper: TestProviders } ); - const caseNavItem = (result.current?.items || [])[3].items?.find( + const caseNavItem = (result.current?.items || [])[4].items?.find( (item) => item['data-test-subj'] === 'navigation-cases' ); expect(caseNavItem).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index 4012bb81466cf..be131f168464a 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -76,10 +76,16 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { { id: 'main', name: '', + items: [navTabs[SecurityPageName.landing]], + }, + { + ...securityNavGroup[SecurityNavGroupKey.dashboards], items: [ - navTabs[SecurityPageName.landing], navTabs[SecurityPageName.overview], navTabs[SecurityPageName.detectionAndResponse], + ...(navTabs[SecurityPageName.kubernetes] != null + ? [navTabs[SecurityPageName.kubernetes]] + : []), ], }, { @@ -98,9 +104,6 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { ...(navTabs[SecurityPageName.users] != null ? [navTabs[SecurityPageName.users]] : []), - ...(navTabs[SecurityPageName.kubernetes] != null - ? [navTabs[SecurityPageName.kubernetes]] - : []), ], }, { diff --git a/x-pack/plugins/security_solution/public/common/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/common/containers/authentications/index.tsx index 408d29f4e972a..499a0cd78e6d1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/authentications/index.tsx @@ -18,7 +18,7 @@ import { UserAuthenticationsStrategyResponse, UsersQueries, } from '../../../../common/search_strategy/security_solution'; -import { PageInfoPaginated, DocValueFields, SortField } from '../../../../common/search_strategy'; +import { PageInfoPaginated, SortField } from '../../../../common/search_strategy'; import { ESTermQuery } from '../../../../common/typed_json'; import { inputsModel } from '../../store'; @@ -43,7 +43,6 @@ export interface AuthenticationArgs { } interface UseAuthentications { - docValueFields?: DocValueFields[]; filterQuery?: ESTermQuery | string; endDate: string; indexNames: string[]; @@ -55,7 +54,6 @@ interface UseAuthentications { } export const useAuthentications = ({ - docValueFields, filterQuery, endDate, indexNames, @@ -163,7 +161,6 @@ export const useAuthentications = ({ const myRequest = { ...(prevRequest ?? {}), defaultIndex: indexNames, - docValueFields: docValueFields ?? [], factoryQueryType: UsersQueries.authentications, filterQuery: createFilter(filterQuery), stackByField, @@ -180,16 +177,7 @@ export const useAuthentications = ({ } return prevRequest; }); - }, [ - activePage, - docValueFields, - endDate, - filterQuery, - indexNames, - stackByField, - limit, - startDate, - ]); + }, [activePage, endDate, filterQuery, indexNames, stackByField, limit, startDate]); useEffect(() => { authenticationsSearch(authenticationsRequest); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/add_to_timeline.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/add_to_timeline.test.tsx new file mode 100644 index 0000000000000..ff1ddbe03cc3e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/add_to_timeline.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; +import { getAddToTimelineCellAction } from './add_to_timeline'; + +jest.mock('../kibana'); + +describe('getAddToTimelineCellAction', () => { + const sampleData: TimelineNonEcsData = { + field: 'fizz', + value: ['buzz'], + }; + const testComponent = () => <>; + const componentProps = { + colIndex: 1, + rowIndex: 1, + columnId: 'fizz', + Component: testComponent, + isExpanded: false, + }; + describe('when data property is', () => { + test('undefined', () => { + const CellComponent = getAddToTimelineCellAction({ pageSize: 1, data: undefined }); + const result = render(); + expect(result.container).toBeEmptyDOMElement(); + }); + + test('empty', () => { + const CellComponent = getAddToTimelineCellAction({ pageSize: 1, data: [] }); + const result = render(); + expect(result.container).toBeEmptyDOMElement(); + }); + }); + + describe('AddToTimelineCellActions', () => { + const data: TimelineNonEcsData[][] = [[sampleData]]; + test('should render with data', () => { + const AddToTimelineCellComponent = getAddToTimelineCellAction({ pageSize: 1, data }); + const result = render(); + expect(result.getByTestId('test-add-to-timeline')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/add_to_timeline.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/add_to_timeline.tsx new file mode 100644 index 0000000000000..f8941b15ab796 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/add_to_timeline.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common/search_strategy'; +import { DataProvider } from '@kbn/timelines-plugin/common/types'; +import { getPageRowIndex } from '@kbn/timelines-plugin/public'; +import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { escapeDataProviderId } from '../../components/drag_and_drop/helpers'; +import { EmptyComponent, useKibanaServices } from './helpers'; + +export const getAddToTimelineCellAction = ({ + data, + pageSize, +}: { + data?: TimelineNonEcsData[][]; + pageSize: number; +}) => + data && data.length > 0 + ? function AddToTimeline({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) { + const { timelines } = useKibanaServices(); + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + const rowData = useMemo(() => { + return { + data: data[pageRowIndex], + fieldName: columnId, + }; + }, [pageRowIndex, columnId]); + + const value = useGetMappedNonEcsValue(rowData); + + const addToTimelineButton = useMemo( + () => timelines.getHoverActions().getAddToTimelineButton, + [timelines] + ); + + const dataProvider: DataProvider[] = useMemo( + () => + value?.map((x) => ({ + and: [], + enabled: true, + id: `${escapeDataProviderId(columnId)}-row-${rowIndex}-col-${columnId}-val-${x}`, + name: x, + excluded: false, + kqlQuery: '', + queryMatch: { + field: columnId, + value: x, + operator: IS_OPERATOR, + }, + })) ?? [], + [columnId, rowIndex, value] + ); + const addToTimelineProps = useMemo(() => { + return { + Component, + dataProvider, + field: columnId, + ownFocus: false, + showTooltip: false, + }; + }, [Component, columnId, dataProvider]); + + // data grid expects each cell action always return an element, it crashes if returns null + return pageRowIndex >= data.length ? ( + <>{EmptyComponent} + ) : ( + <>{addToTimelineButton(addToTimelineProps)} + ); + } + : EmptyComponent; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/copy.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/copy.test.tsx new file mode 100644 index 0000000000000..d7946fb397c62 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/copy.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; +import { getCopyCellAction } from './copy'; + +jest.mock('../kibana'); + +describe('getCopyCellAction', () => { + const sampleData: TimelineNonEcsData = { + field: 'fizz', + value: ['buzz'], + }; + const testComponent = () => <>; + const componentProps = { + colIndex: 1, + rowIndex: 1, + columnId: 'fizz', + Component: testComponent, + isExpanded: false, + }; + describe('when data property is', () => { + test('undefined', () => { + const CellComponent = getCopyCellAction({ pageSize: 1, data: undefined }); + const result = render(); + expect(result.container).toBeEmptyDOMElement(); + }); + + test('empty', () => { + const CellComponent = getCopyCellAction({ pageSize: 1, data: [] }); + const result = render(); + expect(result.container).toBeEmptyDOMElement(); + }); + }); + + describe('CopyCellAction', () => { + const data: TimelineNonEcsData[][] = [[sampleData]]; + test('should render with data', () => { + const CopyCellAction = getCopyCellAction({ pageSize: 1, data }); + const result = render(); + expect(result.getByTestId('test-copy-button')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/copy.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/copy.tsx new file mode 100644 index 0000000000000..753eefc15393a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/copy.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common/search_strategy'; +import { getPageRowIndex } from '@kbn/timelines-plugin/public'; +import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; +import { EmptyComponent, useKibanaServices } from './helpers'; + +export const getCopyCellAction = ({ + data, + pageSize, +}: { + data?: TimelineNonEcsData[][]; + pageSize: number; +}) => + data && data.length > 0 + ? function CopyButton({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) { + const { timelines } = useKibanaServices(); + + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + + const copyButton = useMemo(() => timelines.getHoverActions().getCopyButton, [timelines]); + + const rowData = useMemo(() => { + return { + data: data[pageRowIndex], + fieldName: columnId, + }; + }, [pageRowIndex, columnId]); + + const value = useGetMappedNonEcsValue(rowData); + + const copyButtonProps = useMemo(() => { + return { + Component, + field: columnId, + isHoverAction: false, + ownFocus: false, + showTooltip: false, + value, + }; + }, [Component, columnId, value]); + + // data grid expects each cell action always return an element, it crashes if returns null + return pageRowIndex >= data.length ? ( + <>{EmptyComponent} + ) : ( + <>{copyButton(copyButtonProps)} + ); + } + : EmptyComponent; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.test.tsx index 0467f0f50fc71..ff1fb993effdc 100644 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.test.tsx @@ -13,8 +13,8 @@ import { TGridCellAction } from '@kbn/timelines-plugin/common/types'; import { Ecs } from '../../../../common/ecs'; import { ColumnHeaderType } from '../../../timelines/store/timeline/model'; -import { defaultCellActions, EmptyComponent } from './default_cell_actions'; -import { COLUMNS_WITH_LINKS } from './helpers'; +import { defaultCellActions } from './default_cell_actions'; +import { COLUMNS_WITH_LINKS, EmptyComponent } from './helpers'; describe('default cell actions', () => { const browserFields: BrowserFields = {}; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.ts b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.ts new file mode 100644 index 0000000000000..3992023346cda --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TGridCellAction } from '@kbn/timelines-plugin/common/types'; +import { getFilterForCellAction } from './filter_for'; +import { getFilterOutCellAction } from './filter_out'; +import { getAddToTimelineCellAction } from './add_to_timeline'; +import { getCopyCellAction } from './copy'; +import { FieldValueCell } from './field_value'; + +export const cellActions: TGridCellAction[] = [ + getFilterForCellAction, + getFilterOutCellAction, + getAddToTimelineCellAction, + getCopyCellAction, + FieldValueCell, +]; + +/** the default actions shown in `EuiDataGrid` cells */ +export const defaultCellActions = [...cellActions]; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx deleted file mode 100644 index 55a978a84d25b..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; -import { head, getOr, get, isEmpty } from 'lodash/fp'; -import React, { useMemo } from 'react'; - -import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common/search_strategy'; -import { - ColumnHeaderOptions, - DataProvider, - TGridCellAction, -} from '@kbn/timelines-plugin/common/types'; -import { getPageRowIndex } from '@kbn/timelines-plugin/public'; -import { Ecs } from '../../../../common/ecs'; -import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; -import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; -import { parseValue } from '../../../timelines/components/timeline/body/renderers/parse_value'; -import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { escapeDataProviderId } from '../../components/drag_and_drop/helpers'; -import { useKibana } from '../kibana'; -import { getLinkColumnDefinition } from './helpers'; -import { getField, getFieldKey } from '../../../helpers'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; - -/** a noop required by the filter in / out buttons */ -const onFilterAdded = () => {}; - -/** a hook to eliminate the verbose boilerplate required to use common services */ -const useKibanaServices = () => { - const { - timelines, - data: { - query: { filterManager }, - }, - } = useKibana().services; - - return { timelines, filterManager }; -}; - -export const EmptyComponent = () => <>; - -const useFormattedFieldProps = ({ - rowIndex, - pageSize, - ecsData, - columnId, - header, - data, -}: { - rowIndex: number; - data: TimelineNonEcsData[][]; - ecsData: Ecs[]; - header?: ColumnHeaderOptions; - columnId: string; - pageSize: number; -}) => { - const pageRowIndex = getPageRowIndex(rowIndex, pageSize); - const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); - const ecs = ecsData[pageRowIndex]; - const link = getLinkColumnDefinition(columnId, header?.type, header?.linkField, usersEnabled); - const linkField = header?.linkField ? header?.linkField : link?.linkField; - const linkValues = header && getOr([], linkField ?? '', ecs); - const eventId = (header && get('_id' ?? '', ecs)) || ''; - const rowData = useMemo(() => { - return { - data: data[pageRowIndex], - fieldName: columnId, - }; - }, [pageRowIndex, columnId, data]); - - const values = useGetMappedNonEcsValue(rowData); - const value = parseValue(head(values)); - const title = values && values.length > 1 ? `${link?.label}: ${value}` : link?.label; - // if linkField is defined but link values is empty, it's possible we are trying to look for a column definition for an old event set - if (linkField !== undefined && linkValues.length === 0 && values !== undefined) { - const normalizedLinkValue = getField(ecs, linkField); - const normalizedLinkField = getFieldKey(ecs, linkField); - const normalizedColumnId = getFieldKey(ecs, columnId); - const normalizedLink = getLinkColumnDefinition( - normalizedColumnId, - header?.type, - normalizedLinkField, - usersEnabled - ); - return { - pageRowIndex, - link: normalizedLink, - eventId, - fieldFormat: header?.format || '', - fieldName: normalizedColumnId, - fieldType: header?.type || '', - value: parseValue(head(normalizedColumnId)), - values, - title, - linkValue: head(normalizedLinkValue), - }; - } else { - return { - pageRowIndex, - link, - eventId, - fieldFormat: header?.format || '', - fieldName: columnId, - fieldType: header?.type || '', - value, - values, - title, - linkValue: head(linkValues), - }; - } -}; - -export const cellActions: TGridCellAction[] = [ - ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => - function FilterFor({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) { - const { timelines, filterManager } = useKibanaServices(); - - const pageRowIndex = getPageRowIndex(rowIndex, pageSize); - const rowData = useMemo(() => { - return { - data: data[pageRowIndex], - fieldName: columnId, - }; - }, [pageRowIndex, columnId]); - - const value = useGetMappedNonEcsValue(rowData); - const filterForButton = useMemo( - () => timelines.getHoverActions().getFilterForValueButton, - [timelines] - ); - - const filterForProps = useMemo(() => { - return { - Component, - field: columnId, - filterManager, - onFilterAdded, - ownFocus: false, - showTooltip: false, - value, - }; - }, [Component, columnId, filterManager, value]); - if (pageRowIndex >= data.length) { - // data grid expects each cell action always return an element, it crashes if returns null - return <>; - } - - return <>{filterForButton(filterForProps)}; - }, - ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => - function FilterOut({ rowIndex, columnId, Component }) { - const { timelines, filterManager } = useKibanaServices(); - const pageRowIndex = getPageRowIndex(rowIndex, pageSize); - - const rowData = useMemo(() => { - return { - data: data[pageRowIndex], - fieldName: columnId, - }; - }, [pageRowIndex, columnId]); - - const value = useGetMappedNonEcsValue(rowData); - - const filterOutButton = useMemo( - () => timelines.getHoverActions().getFilterOutValueButton, - [timelines] - ); - - const filterOutProps = useMemo(() => { - return { - Component, - field: columnId, - filterManager, - onFilterAdded, - ownFocus: false, - showTooltip: false, - value, - }; - }, [Component, columnId, filterManager, value]); - if (pageRowIndex >= data.length) { - // data grid expects each cell action always return an element, it crashes if returns null - return <>; - } - - return <>{filterOutButton(filterOutProps)}; - }, - ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => - function AddToTimeline({ rowIndex, columnId, Component }) { - const { timelines } = useKibanaServices(); - - const pageRowIndex = getPageRowIndex(rowIndex, pageSize); - const rowData = useMemo(() => { - return { - data: data[pageRowIndex], - fieldName: columnId, - }; - }, [pageRowIndex, columnId]); - - const value = useGetMappedNonEcsValue(rowData); - - const addToTimelineButton = useMemo( - () => timelines.getHoverActions().getAddToTimelineButton, - [timelines] - ); - - const dataProvider: DataProvider[] = useMemo( - () => - value?.map((x) => ({ - and: [], - enabled: true, - id: `${escapeDataProviderId(columnId)}-row-${rowIndex}-col-${columnId}-val-${x}`, - name: x, - excluded: false, - kqlQuery: '', - queryMatch: { - field: columnId, - value: x, - operator: IS_OPERATOR, - }, - })) ?? [], - [columnId, rowIndex, value] - ); - const addToTimelineProps = useMemo(() => { - return { - Component, - dataProvider, - field: columnId, - ownFocus: false, - showTooltip: false, - }; - }, [Component, columnId, dataProvider]); - if (pageRowIndex >= data.length) { - // data grid expects each cell action always return an element, it crashes if returns null - return <>; - } - - return <>{addToTimelineButton(addToTimelineProps)}; - }, - ({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) => - function CopyButton({ rowIndex, columnId, Component }) { - const { timelines } = useKibanaServices(); - - const pageRowIndex = getPageRowIndex(rowIndex, pageSize); - - const copyButton = useMemo(() => timelines.getHoverActions().getCopyButton, [timelines]); - - const rowData = useMemo(() => { - return { - data: data[pageRowIndex], - fieldName: columnId, - }; - }, [pageRowIndex, columnId]); - - const value = useGetMappedNonEcsValue(rowData); - - const copyButtonProps = useMemo(() => { - return { - Component, - field: columnId, - isHoverAction: false, - ownFocus: false, - showTooltip: false, - value, - }; - }, [Component, columnId, value]); - if (pageRowIndex >= data.length) { - // data grid expects each cell action always return an element, it crashes if returns null - return <>; - } - - return <>{copyButton(copyButtonProps)}; - }, - ({ - data, - ecsData, - header, - timelineId, - pageSize, - closeCellPopover, - }: { - data: TimelineNonEcsData[][]; - ecsData: Ecs[]; - header?: ColumnHeaderOptions; - timelineId: string; - pageSize: number; - closeCellPopover?: () => void; - }) => { - if (header !== undefined) { - return function FieldValue({ - rowIndex, - columnId, - Component, - }: EuiDataGridColumnCellActionProps) { - const { - pageRowIndex, - link, - eventId, - value, - values, - title, - fieldName, - fieldFormat, - fieldType, - linkValue, - } = useFormattedFieldProps({ rowIndex, pageSize, ecsData, columnId, header, data }); - - const showEmpty = useMemo(() => { - const hasLink = link !== undefined && values && !isEmpty(value); - if (pageRowIndex >= data.length) { - return true; - } else { - return hasLink !== true; - } - }, [link, pageRowIndex, value, values]); - - return showEmpty === false ? ( - - ) : ( - // data grid expects each cell action always return an element, it crashes if returns null - <> - ); - }; - } else { - return EmptyComponent; - } - }, -]; - -/** the default actions shown in `EuiDataGrid` cells */ -export const defaultCellActions = [...cellActions]; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/field_value.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/field_value.tsx new file mode 100644 index 0000000000000..b9a1e9b6cb126 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/field_value.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { head, getOr, get, isEmpty } from 'lodash/fp'; +import React, { useMemo } from 'react'; + +import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common/search_strategy'; +import { ColumnHeaderOptions } from '@kbn/timelines-plugin/common/types'; +import { getPageRowIndex } from '@kbn/timelines-plugin/public'; +import { Ecs } from '../../../../common/ecs'; +import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; +import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; +import { parseValue } from '../../../timelines/components/timeline/body/renderers/parse_value'; +import { EmptyComponent, getLinkColumnDefinition } from './helpers'; +import { getField, getFieldKey } from '../../../helpers'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; + +const useFormattedFieldProps = ({ + rowIndex, + pageSize, + ecsData, + columnId, + header, + data, +}: { + rowIndex: number; + data: TimelineNonEcsData[][]; + ecsData: Ecs[]; + header?: ColumnHeaderOptions; + columnId: string; + pageSize: number; +}) => { + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); + const ecs = ecsData[pageRowIndex]; + const link = getLinkColumnDefinition(columnId, header?.type, header?.linkField, usersEnabled); + const linkField = header?.linkField ? header?.linkField : link?.linkField; + const linkValues = header && getOr([], linkField ?? '', ecs); + const eventId = (header && get('_id' ?? '', ecs)) || ''; + const rowData = useMemo(() => { + return { + data: data[pageRowIndex], + fieldName: columnId, + }; + }, [pageRowIndex, columnId, data]); + + const values = useGetMappedNonEcsValue(rowData); + const value = parseValue(head(values)); + const title = values && values.length > 1 ? `${link?.label}: ${value}` : link?.label; + // if linkField is defined but link values is empty, it's possible we are trying to look for a column definition for an old event set + if (linkField !== undefined && linkValues.length === 0 && values !== undefined) { + const normalizedLinkValue = getField(ecs, linkField); + const normalizedLinkField = getFieldKey(ecs, linkField); + const normalizedColumnId = getFieldKey(ecs, columnId); + const normalizedLink = getLinkColumnDefinition( + normalizedColumnId, + header?.type, + normalizedLinkField, + usersEnabled + ); + return { + pageRowIndex, + link: normalizedLink, + eventId, + fieldFormat: header?.format || '', + fieldName: normalizedColumnId, + fieldType: header?.type || '', + value: parseValue(head(normalizedColumnId)), + values, + title, + linkValue: head(normalizedLinkValue), + }; + } else { + return { + pageRowIndex, + link, + eventId, + fieldFormat: header?.format || '', + fieldName: columnId, + fieldType: header?.type || '', + value, + values, + title, + linkValue: head(linkValues), + }; + } +}; + +export const FieldValueCell = ({ + data, + ecsData, + header, + timelineId, + pageSize, + closeCellPopover, +}: { + data: TimelineNonEcsData[][]; + ecsData: Ecs[]; + header?: ColumnHeaderOptions; + timelineId: string; + pageSize: number; + closeCellPopover?: () => void; +}) => { + if (header !== undefined) { + return function FieldValue({ + rowIndex, + columnId, + Component, + }: EuiDataGridColumnCellActionProps) { + const { + pageRowIndex, + link, + eventId, + value, + values, + title, + fieldName, + fieldFormat, + fieldType, + linkValue, + } = useFormattedFieldProps({ rowIndex, pageSize, ecsData, columnId, header, data }); + + const showEmpty = useMemo(() => { + const hasLink = link !== undefined && values && !isEmpty(value); + if (pageRowIndex >= data.length) { + return true; + } else { + return hasLink !== true; + } + }, [link, pageRowIndex, value, values]); + + return showEmpty === false ? ( + + ) : ( + // data grid expects each cell action always return an element, it crashes if returns null + EmptyComponent + ); + }; + } else { + return EmptyComponent; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_for.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_for.test.tsx new file mode 100644 index 0000000000000..d88f5f855bac6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_for.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; +import { getFilterForCellAction } from './filter_for'; + +jest.mock('../kibana'); + +describe('getFilterForCellAction', () => { + const sampleData: TimelineNonEcsData = { + field: 'fizz', + value: ['buzz'], + }; + const testComponent = () => <>; + const componentProps = { + colIndex: 1, + rowIndex: 1, + columnId: 'fizz', + Component: testComponent, + isExpanded: false, + }; + describe('when data property is', () => { + test('undefined', () => { + const CellComponent = getFilterForCellAction({ pageSize: 1, data: undefined }); + const result = render(); + expect(result.container).toBeEmptyDOMElement(); + }); + + test('empty', () => { + const CellComponent = getFilterForCellAction({ pageSize: 1, data: [] }); + const result = render(); + expect(result.container).toBeEmptyDOMElement(); + }); + }); + + describe('FilterForCellAction', () => { + const data: TimelineNonEcsData[][] = [[sampleData]]; + test('should render with data', () => { + const FilterForCellAction = getFilterForCellAction({ pageSize: 1, data }); + const result = render(); + expect(result.getByTestId('test-filter-for')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_for.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_for.tsx new file mode 100644 index 0000000000000..39e1c98375c6d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_for.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common/search_strategy'; +import { getPageRowIndex } from '@kbn/timelines-plugin/public'; +import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; +import { EmptyComponent, onFilterAdded, useKibanaServices } from './helpers'; + +export const getFilterForCellAction = ({ + data, + pageSize, +}: { + data?: TimelineNonEcsData[][]; + pageSize: number; +}) => + data && data.length > 0 + ? function FilterFor({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) { + const { timelines, filterManager } = useKibanaServices(); + + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + const rowData = useMemo(() => { + return { + data: data[pageRowIndex], + fieldName: columnId, + }; + }, [pageRowIndex, columnId]); + + const value = useGetMappedNonEcsValue(rowData); + const filterForButton = useMemo( + () => timelines.getHoverActions().getFilterForValueButton, + [timelines] + ); + + const filterForProps = useMemo(() => { + return { + Component, + field: columnId, + filterManager, + onFilterAdded, + ownFocus: false, + showTooltip: false, + value, + }; + }, [Component, columnId, filterManager, value]); + + // data grid expects each cell action always return an element, it crashes if returns null + return pageRowIndex >= data.length ? ( + <>{EmptyComponent} + ) : ( + <>{filterForButton(filterForProps)} + ); + } + : EmptyComponent; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_out.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_out.test.tsx new file mode 100644 index 0000000000000..f8f66f1f38137 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_out.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; +import { getFilterOutCellAction } from './filter_out'; + +jest.mock('../kibana'); + +describe('getFilterOutCellAction', () => { + const sampleData: TimelineNonEcsData = { + field: 'fizz', + value: ['buzz'], + }; + const testComponent = () => <>; + const componentProps = { + colIndex: 1, + rowIndex: 1, + columnId: 'fizz', + Component: testComponent, + isExpanded: false, + }; + describe('when data property is', () => { + test('undefined', () => { + const CellComponent = getFilterOutCellAction({ pageSize: 1, data: undefined }); + const result = render(); + expect(result.container).toBeEmptyDOMElement(); + }); + + test('empty', () => { + const CellComponent = getFilterOutCellAction({ pageSize: 1, data: [] }); + const result = render(); + expect(result.container).toBeEmptyDOMElement(); + }); + }); + + describe('FilterOutCellAction', () => { + const data: TimelineNonEcsData[][] = [[sampleData]]; + test('should render with data', () => { + const FilterOutCellAction = getFilterOutCellAction({ pageSize: 1, data }); + const result = render(); + expect(result.getByTestId('test-filter-out')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_out.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_out.tsx new file mode 100644 index 0000000000000..edb21075ea6ee --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/filter_out.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common/search_strategy'; +import { getPageRowIndex } from '@kbn/timelines-plugin/public'; +import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; +import { EmptyComponent, onFilterAdded, useKibanaServices } from './helpers'; + +export const getFilterOutCellAction = ({ + data, + pageSize, +}: { + data?: TimelineNonEcsData[][]; + pageSize: number; +}) => + data && data.length > 0 + ? function FilterOut({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) { + const { timelines, filterManager } = useKibanaServices(); + const pageRowIndex = getPageRowIndex(rowIndex, pageSize); + + const rowData = useMemo(() => { + return { + data: data[pageRowIndex], + fieldName: columnId, + }; + }, [pageRowIndex, columnId]); + + const value = useGetMappedNonEcsValue(rowData); + + const filterOutButton = useMemo( + () => timelines.getHoverActions().getFilterOutValueButton, + [timelines] + ); + + const filterOutProps = useMemo(() => { + return { + Component, + field: columnId, + filterManager, + onFilterAdded, + ownFocus: false, + showTooltip: false, + value, + }; + }, [Component, columnId, filterManager, value]); + + // data grid expects each cell action always return an element, it crashes if returns null + return pageRowIndex >= data.length ? ( + <>{EmptyComponent} + ) : ( + <>{filterOutButton(filterOutProps)} + ); + } + : EmptyComponent; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.ts b/x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.tsx similarity index 83% rename from x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.ts rename to x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.tsx index 2d5d21e708fee..507e34a9ccc01 100644 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import * as i18n from './translations'; import { EVENT_URL_FIELD_NAME, @@ -17,6 +18,7 @@ import { import { INDICATOR_REFERENCE } from '../../../../common/cti/constants'; import { IP_FIELD_TYPE } from '../../../network/components/ip'; import { PORT_NAMES } from '../../../network/components/port/helpers'; +import { useKibana } from '../kibana'; export const COLUMNS_WITH_LINKS = [ { @@ -96,3 +98,20 @@ export const getLinkColumnDefinition = ( } }); }; + +/** a noop required by the filter in / out buttons */ +export const onFilterAdded = () => {}; + +/** a hook to eliminate the verbose boilerplate required to use common services */ +export const useKibanaServices = () => { + const { + timelines, + data: { + query: { filterManager }, + }, + } = useKibana().services; + + return { timelines, filterManager }; +}; + +export const EmptyComponent = () => <>; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts index e134b58be9605..a0bacb94fb19a 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts @@ -22,6 +22,7 @@ export const getDetectionAlertMock = (overrides: Partial = {}): Ecs => ({ category: ['Access'], module: ['nginx'], severity: [3], + kind: ['signal'], }, source: { ip: ['192.168.0.1'], diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index ea9b165d0d0f7..1cb456a09c634 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -640,6 +640,7 @@ export const mockTimelineData: TimelineItem[] = [ { field: '@timestamp', value: ['2019-03-13T05:42:11.815Z'] }, { field: 'event.category', value: ['audit-rule'] }, { field: 'host.name', value: ['zeek-sanfran'] }, + { field: 'process.args', value: ['gpgconf', '--list-dirs', 'agent-socket'] }, ], ecs: { _id: '20', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index d8bf2cff59aaf..387e464310ca8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -133,6 +133,48 @@ describe('alert actions', () => { }, }); + const ecsDataMockWithNoTemplateTimelineAndNoFilters = getThresholdDetectionAlertAADMock({ + ...mockAADEcsDataWithAlert, + kibana: { + alert: { + ...mockAADEcsDataWithAlert.kibana?.alert, + rule: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule, + parameters: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, + threshold: { + field: ['destination.ip'], + value: 1, + }, + filters: undefined, + }, + name: ['mock threshold rule'], + saved_id: [], + type: ['threshold'], + uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], + timeline_id: undefined, + timeline_title: undefined, + }, + threshold_result: { + count: 99, + from: '2021-01-10T21:11:45.839Z', + cardinality: [ + { + field: 'source.ip', + value: 1, + }, + ], + terms: [ + { + field: 'destination.ip', + value: 1, + }, + ], + }, + }, + }, + }); + beforeEach(() => { // jest carries state between mocked implementations when using // spyOn. So now we're doing all three of these. @@ -533,7 +575,7 @@ describe('alert actions', () => { }); describe('Threshold', () => { - beforeEach(() => { + test('Exceptions and filters are included', async () => { fetchMock.mockResolvedValue({ hits: { hits: [ @@ -545,9 +587,6 @@ describe('alert actions', () => { ], }, }); - }); - - test('Exceptions and filters are included', async () => { mockGetExceptions.mockResolvedValue([getExceptionListItemSchemaMock()]); await sendAlertToTimelineAction({ createTimeline, @@ -664,6 +703,31 @@ describe('alert actions', () => { to: expectedTo, }); }); + + test('Does not crash when no filters provided', async () => { + fetchMock.mockResolvedValue({ + hits: { + hits: [ + { + _id: ecsDataMockWithNoTemplateTimelineAndNoFilters[0]._id, + _index: 'mock', + _source: ecsDataMockWithNoTemplateTimelineAndNoFilters[0], + }, + ], + }, + }); + mockGetExceptions.mockResolvedValue([getExceptionListItemSchemaMock()]); + await sendAlertToTimelineAction({ + createTimeline, + ecsData: ecsDataMockWithNoTemplateTimelineAndNoFilters, + updateTimelineIsLoading, + searchStrategyClient, + getExceptions: mockGetExceptions, + }); + + expect(createTimeline).not.toThrow(); + expect(toastMock).not.toHaveBeenCalled(); + }); }); describe('determineToAndFrom', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index c4b8d30f60635..36c31d01c8256 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -387,6 +387,10 @@ const buildEqlDataProviderOrFilter = ( return { filters: [], dataProviders: [] }; }; +interface MightHaveFilters { + filters?: Filter[]; +} + const createThresholdTimeline = async ( ecsData: Ecs, createTimeline: ({ from, timeline, to }: CreateTimelineProps) => void, @@ -416,7 +420,10 @@ const createThresholdTimeline = async ( const alertDoc = formattedAlertData[0]; const params = getField(alertDoc, ALERT_RULE_PARAMETERS); - const filters: Filter[] = params.filters ?? alertDoc.signal?.rule?.filters; + const filters: Filter[] = + (params as MightHaveFilters).filters ?? + (alertDoc.signal?.rule as MightHaveFilters)?.filters ?? + []; // https://github.com/elastic/kibana/issues/126574 - if the provided filter has no `meta` field // we expect an empty object to be inserted before calling `createTimeline` const augmentedFilters = filters.map((filter) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 674bcdab5e415..3f6668b6e1e23 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -30,6 +30,9 @@ const ecsRowData: Ecs = { }, }, }, + event: { + kind: ['signal'], + }, }; const props = { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx index e163a679eb253..5c2777febfb71 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx @@ -37,6 +37,10 @@ export const useAddToCaseActions = ({ const casePermissions = useGetUserCasesPermissions(); const hasWritePermissions = casePermissions?.crud ?? false; + const isAlert = useMemo(() => { + return ecsData?.event?.kind?.includes('signal'); + }, [ecsData]); + const caseAttachments: CaseAttachments = useMemo(() => { return ecsData?._id ? [ @@ -80,7 +84,8 @@ export const useAddToCaseActions = ({ TimelineId.detectionsRulesDetailsPage, TimelineId.active, ].includes(timelineId as TimelineId) && - hasWritePermissions + hasWritePermissions && + isAlert ) { return [ // add to existing case menu item @@ -110,6 +115,7 @@ export const useAddToCaseActions = ({ handleAddToNewCaseClick, hasWritePermissions, timelineId, + isAlert, ]); return { diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/flyout.test.tsx similarity index 78% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/flyout.test.tsx index 164d333650443..1268e5170bb02 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/flyout.test.tsx @@ -15,7 +15,7 @@ import { exportList } from '@kbn/securitysolution-list-api'; import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types'; import { TestProviders } from '../../../common/mock'; -import { ValueListsModal } from './modal'; +import { ValueListsFlyout } from './flyout'; jest.mock('@kbn/securitysolution-list-hooks', () => { const actual = jest.requireActual('@kbn/securitysolution-list-hooks'); @@ -36,7 +36,7 @@ jest.mock('@kbn/securitysolution-list-api', () => { }; }); -describe('ValueListsModal', () => { +describe('ValueListsFlyout', () => { beforeEach(() => { // Do not resolve the export in tests as it causes unexpected state updates (exportList as jest.Mock).mockImplementation(() => new Promise(() => {})); @@ -50,35 +50,35 @@ describe('ValueListsModal', () => { }); }); - it('renders nothing if showModal is false', () => { + it('renders nothing if showFlyout is false', () => { const container = mount( - + ); - expect(container.find('EuiModal')).toHaveLength(0); + expect(container.find('EuiFlyout')).toHaveLength(0); }); - it('renders modal if showModal is true', () => { + it('renders flyout if showFlyout is true', () => { const container = mount( - + ); - expect(container.find('EuiModal')).toHaveLength(1); + expect(container.find('EuiFlyout')).toHaveLength(1); }); - it('calls onClose when modal is closed', () => { + it('calls onClose when flyout is closed', () => { const onClose = jest.fn(); const container = mount( - + ); - container.find('button[data-test-subj="value-lists-modal-close-action"]').simulate('click'); + container.find('button[data-test-subj="value-lists-flyout-close-action"]').simulate('click'); expect(onClose).toHaveBeenCalled(); }); @@ -86,7 +86,7 @@ describe('ValueListsModal', () => { it('renders ValueListsForm and an EuiTable', () => { const container = mount( - + ); @@ -94,11 +94,11 @@ describe('ValueListsModal', () => { expect(container.find('EuiBasicTable')).toHaveLength(1); }); - describe('modal table actions', () => { + describe('flyout table actions', () => { it('calls exportList when export is clicked', async () => { const container = mount( - + ); @@ -120,7 +120,7 @@ describe('ValueListsModal', () => { }); const container = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/flyout.tsx similarity index 78% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/flyout.tsx index 4fc4653489845..49fbe9a9be8f3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/flyout.tsx @@ -10,12 +10,11 @@ import { isEmpty } from 'lodash/fp'; import { EuiBasicTable, EuiButton, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiPanel, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTitle, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -33,28 +32,28 @@ import { ValueListsForm } from './form'; import { ReferenceErrorModal } from './reference_error_modal'; import { AutoDownload } from '../../../common/components/auto_download/auto_download'; -interface ValueListsModalProps { +interface ValueListsFlyoutProps { onClose: () => void; - showModal: boolean; + showFlyout: boolean; } -interface ReferenceModalState { +interface ReferenceFlyoutState { contentText: string; exceptionListReferences: string[]; isLoading: boolean; valueListId: string; } -const referenceModalInitialState: ReferenceModalState = { +const referenceModalInitialState: ReferenceFlyoutState = { contentText: '', exceptionListReferences: [], isLoading: false, valueListId: '', }; -export const ValueListsModalComponent: React.FC = ({ +export const ValueListsFlyoutComponent: React.FC = ({ onClose, - showModal, + showFlyout, }) => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(5); @@ -67,7 +66,7 @@ export const ValueListsModalComponent: React.FC = ({ const [exportDownload, setExportDownload] = useState<{ name?: string; blob?: Blob }>({}); const { addError, addSuccess } = useAppToasts(); const [showReferenceErrorModal, setShowReferenceErrorModal] = useState(false); - const [referenceModalState, setReferenceModalState] = useState( + const [referenceFlyoutState, setReferenceFlyoutState] = useState( referenceModalInitialState ); @@ -92,10 +91,10 @@ export const ValueListsModalComponent: React.FC = ({ const handleReferenceDelete = useCallback(async () => { setShowReferenceErrorModal(false); - deleteList({ deleteReferences: true, http, id: referenceModalState.valueListId }); - setReferenceModalState(referenceModalInitialState); + deleteList({ deleteReferences: true, http, id: referenceFlyoutState.valueListId }); + setReferenceFlyoutState(referenceModalInitialState); setDeletingListIds([]); - }, [deleteList, http, referenceModalState.valueListId]); + }, [deleteList, http, referenceFlyoutState.valueListId]); useEffect(() => { if (deleteResult != null) { @@ -116,7 +115,7 @@ export const ValueListsModalComponent: React.FC = ({ ) ?? []; const uniqueExceptionListReferences = Array.from(new Set(references)); setShowReferenceErrorModal(true); - setReferenceModalState({ + setReferenceFlyoutState({ contentText: i18n.referenceErrorMessage(uniqueExceptionListReferences.length), exceptionListReferences: uniqueExceptionListReferences, isLoading: false, @@ -170,10 +169,10 @@ export const ValueListsModalComponent: React.FC = ({ ); useEffect(() => { - if (showModal) { + if (showFlyout) { fetchLists(); } - }, [showModal, fetchLists]); + }, [showFlyout, fetchLists]); useEffect(() => { if (!lists.loading && lists.result?.cursor) { @@ -184,7 +183,7 @@ export const ValueListsModalComponent: React.FC = ({ const handleCloseReferenceErrorModal = useCallback(() => { setDeletingListIds([]); setShowReferenceErrorModal(false); - setReferenceModalState({ + setReferenceFlyoutState({ contentText: '', exceptionListReferences: [], isLoading: false, @@ -192,7 +191,7 @@ export const ValueListsModalComponent: React.FC = ({ }); }, []); - if (!showModal) { + if (!showFlyout) { return null; } @@ -212,41 +211,41 @@ export const ValueListsModalComponent: React.FC = ({ return ( <> - - - {i18n.MODAL_TITLE} - - + + + +

{i18n.VALUE_LISTS_FLYOUT_TITLE}

+
+
+ - - -

{i18n.TABLE_TITLE}

-
- -
-
- - + +

{i18n.TABLE_TITLE}

+
+ + + + {i18n.CLOSE_BUTTON} -
-
+ + @@ -259,8 +258,8 @@ export const ValueListsModalComponent: React.FC = ({ ); }; -ValueListsModalComponent.displayName = 'ValueListsModalComponent'; +ValueListsFlyoutComponent.displayName = 'ValueListsFlyoutComponent'; -export const ValueListsModal = React.memo(ValueListsModalComponent); +export const ValueListsFlyout = React.memo(ValueListsFlyoutComponent); -ValueListsModal.displayName = 'ValueListsModal'; +ValueListsFlyout.displayName = 'ValueListsFlyout'; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/form.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/form.test.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/form.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/form.tsx diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/index.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/index.tsx similarity index 84% rename from x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/index.tsx index e17aca73a6d4d..ec87a77632343 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/drilldown_form/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export * from './drilldown_form'; +export { ValueListsFlyout } from './flyout'; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/index.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/reference_error_modal/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/reference_error_modal/index.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/reference_error_modal/reference_error_modal.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/reference_error_modal/reference_error_modal.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/table_helpers.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/table_helpers.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/translations.ts similarity index 85% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/translations.ts index e744abf817460..59cda7ca53ce7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/translations.ts @@ -7,14 +7,17 @@ import { i18n } from '@kbn/i18n'; -export const MODAL_TITLE = i18n.translate('xpack.securitySolution.lists.uploadValueListTitle', { - defaultMessage: 'Upload value lists', -}); +export const VALUE_LISTS_FLYOUT_TITLE = i18n.translate( + 'xpack.securitySolution.lists.importValueListTitle', + { + defaultMessage: 'Import value lists', + } +); export const FILE_PICKER_LABEL = i18n.translate( - 'xpack.securitySolution.lists.uploadValueListDescription', + 'xpack.securitySolution.lists.importValueListDescription', { - defaultMessage: 'Upload single value lists to use while writing rule exceptions.', + defaultMessage: 'Import single value lists to use while writing rule exceptions.', } ); @@ -39,20 +42,20 @@ export const CLOSE_BUTTON = i18n.translate( ); export const CANCEL_BUTTON = i18n.translate( - 'xpack.securitySolution.lists.cancelValueListsUploadTitle', + 'xpack.securitySolution.lists.cancelValueListsImportTitle', { - defaultMessage: 'Cancel upload', + defaultMessage: 'Cancel import', } ); -export const UPLOAD_BUTTON = i18n.translate('xpack.securitySolution.lists.valueListsUploadButton', { - defaultMessage: 'Upload list', +export const UPLOAD_BUTTON = i18n.translate('xpack.securitySolution.lists.valueListsImportButton', { + defaultMessage: 'Import list', }); export const UPLOAD_SUCCESS_TITLE = i18n.translate( - 'xpack.securitySolution.lists.valueListsUploadSuccessTitle', + 'xpack.securitySolution.lists.valueListsImportSuccessTitle', { - defaultMessage: 'Value list uploaded', + defaultMessage: 'Value list imported', } ); @@ -61,8 +64,8 @@ export const UPLOAD_ERROR = i18n.translate('xpack.securitySolution.lists.valueLi }); export const uploadSuccessMessage = (fileName: string) => - i18n.translate('xpack.securitySolution.lists.valueListsUploadSuccess', { - defaultMessage: "Value list '{fileName}' was uploaded", + i18n.translate('xpack.securitySolution.lists.valueListsImportSuccess', { + defaultMessage: "Value list '{fileName}' was imported", values: { fileName }, }); @@ -85,9 +88,9 @@ export const COLUMN_TYPE = i18n.translate( ); export const COLUMN_UPLOAD_DATE = i18n.translate( - 'xpack.securitySolution.lists.valueListsTable.uploadDateColumn', + 'xpack.securitySolution.lists.valueListsTable.importDateColumn', { - defaultMessage: 'Upload Date', + defaultMessage: 'Import Date', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/types.ts b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/types.ts rename to x-pack/plugins/security_solution/public/detections/components/value_lists_management_flyout/types.ts diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 33dff406734c9..85612c124f24c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -149,7 +149,7 @@ export const getAllExceptionListsColumns = ( namespaceType, })} aria-label="Export exception list" - iconType="download" + iconType="exportAction" data-test-subj="exceptionsTableExportButton" /> ), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 0d01872a904e3..8521358dac1f1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -30,7 +30,7 @@ import * as i18n from './translations'; import { AllRulesUtilityBar } from '../utility_bar'; import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns'; import { useAllExceptionLists } from './use_all_exception_lists'; -import { ReferenceErrorModal } from '../../../../../components/value_lists_management_modal/reference_error_modal'; +import { ReferenceErrorModal } from '../../../../../components/value_lists_management_flyout/reference_error_modal'; import { patchRule } from '../../../../../containers/detection_engine/rules/api'; import { ExceptionsSearchBar } from './exceptions_search_bar'; import { getSearchFilters } from '../helpers'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 93d0e73c3017f..226134bc237b0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -74,9 +74,9 @@ jest.mock('../../../containers/detection_engine/rules/api', () => ({ createPrepackagedRules: jest.fn(), })); -jest.mock('../../../components/value_lists_management_modal', () => { +jest.mock('../../../components/value_lists_management_flyout', () => { return { - ValueListsModal: jest.fn().mockReturnValue(
), + ValueListsFlyout: jest.fn().mockReturnValue(
), }; }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 9281dbde77c2a..d4691fe90e7af 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -17,7 +17,7 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { useUserData } from '../../../components/user_info'; import { AllRules } from './all'; import { ImportDataModal } from '../../../../common/components/import_data_modal'; -import { ValueListsModal } from '../../../components/value_lists_management_modal'; +import { ValueListsFlyout } from '../../../components/value_lists_management_flyout'; import { UpdatePrePackagedRulesCallOut } from '../../../components/rules/pre_packaged_rules/update_callout'; import { getPrePackagedRuleStatus, @@ -40,7 +40,7 @@ import { useBoolState } from '../../../../common/hooks/use_bool_state'; const RulesPageComponent: React.FC = () => { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); - const [isValueListModalVisible, showValueListModal, hideValueListModal] = useBoolState(); + const [isValueListFlyoutVisible, showValueListFlyout, hideValueListFlyout] = useBoolState(); const { navigateToApp } = useKibana().services.application; const invalidateRules = useInvalidateRules(); @@ -146,7 +146,7 @@ const RulesPageComponent: React.FC = () => { - + { data-test-subj="open-value-lists-modal-button" iconType="importAction" isDisabled={!canWriteListsIndex || !canUserCRUD || loading} - onClick={showValueListModal} + onClick={showValueListFlyout} > - {i18n.UPLOAD_VALUE_LISTS} + {i18n.IMPORT_VALUE_LISTS} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 7d5a9db2d6842..70aea0c0adf0a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -26,10 +26,10 @@ export const IMPORT_RULE = i18n.translate( } ); -export const UPLOAD_VALUE_LISTS = i18n.translate( - 'xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButton', +export const IMPORT_VALUE_LISTS = i18n.translate( + 'xpack.securitySolution.lists.detectionEngine.rules.importValueListsButton', { - defaultMessage: 'Upload value lists', + defaultMessage: 'Import value lists', } ); diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx index bb5d9b75a66a3..b64271de69ace 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx @@ -8,10 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { - HostsUncommonProcessesEdges, - HostsUncommonProcessItem, -} from '../../../../common/search_strategy'; +import { HostsUncommonProcessesEdges } from '../../../../common/search_strategy'; import { hostsActions, hostsModel, hostsSelectors } from '../../store'; import { defaultToEmptyTag, getEmptyValue } from '../../../common/components/empty_value'; import { HostDetailsLink } from '../../../common/components/links'; @@ -21,6 +18,7 @@ import * as i18n from './translations'; import { getRowItemDraggables } from '../../../common/components/tables/helpers'; import { HostsType } from '../../store/model'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { HostEcs } from '../../../../common/ecs/host'; const tableType = hostsModel.HostsTableType.uncommonProcesses; interface UncommonProcessTableProps { @@ -180,7 +178,7 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ mobileOptions: { show: true }, render: ({ node }) => getRowItemDraggables({ - rowItems: getHostNames(node), + rowItems: getHostNames(node.hosts), attrName: 'host.name', idPrefix: `uncommon-process-table-${node._id}-processHost`, render: (item) => , @@ -219,9 +217,9 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ }, ]; -export const getHostNames = (node: HostsUncommonProcessItem): string[] => { - if (node.hosts != null) { - return node.hosts +export const getHostNames = (hosts: HostEcs[]): string[] => { + if (hosts != null) { + return hosts .filter((host) => host.name != null && host.name[0] != null) .map((host) => (host.name != null && host.name[0] != null ? host.name[0] : '')); } else { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index a2c5cb20d2bca..fdd0dc10601d8 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -18,7 +18,6 @@ import { generateTablePaginationOptions } from '../../../common/components/pagin import { createFilter } from '../../../common/containers/helpers'; import { hostsModel, hostsSelectors } from '../../store'; import { - DocValueFields, SortField, PageInfoPaginated, HostsUncommonProcessesEdges, @@ -48,7 +47,6 @@ export interface UncommonProcessesArgs { } interface UseUncommonProcesses { - docValueFields?: DocValueFields[]; filterQuery?: ESTermQuery | string; endDate: string; indexNames: string[]; @@ -58,7 +56,6 @@ interface UseUncommonProcesses { } export const useUncommonProcesses = ({ - docValueFields, filterQuery, endDate, indexNames, @@ -176,7 +173,6 @@ export const useUncommonProcesses = ({ const myRequest = { ...(prevRequest ?? {}), defaultIndex: indexNames, - docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.uncommonProcesses, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), @@ -192,7 +188,7 @@ export const useUncommonProcesses = ({ } return prevRequest; }); - }, [activePage, indexNames, docValueFields, endDate, filterQuery, limit, startDate]); + }, [activePage, indexNames, endDate, filterQuery, limit, startDate]); useEffect(() => { uncommonProcessesSearch(uncommonProcessesRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index 5b1f84fe5373c..33b98fe193f30 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -34,7 +34,6 @@ import { TimelineId } from '../../../../common/types'; export const HostDetailsTabs = React.memo( ({ detailName, - docValueFields, filterQuery, indexNames, indexPattern, @@ -88,7 +87,7 @@ export const HostDetailsTabs = React.memo( return ( - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 1ee0821579646..f5fdef3d5aa04 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -209,7 +209,6 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta ; export type HostDetailsTabsProps = HostBodyComponentDispatchProps & HostsQueryProps & { - docValueFields?: DocValueFields[]; indexNames: string[]; pageFilters?: Filter[]; filterQuery?: string; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index efeff5cdea54d..ac04e0d3d9acc 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -232,7 +232,6 @@ const HostsComponent = () => { ( ({ deleteQuery, - docValueFields, filterQuery, pageFilters, from, @@ -86,10 +85,10 @@ export const HostsTabs = memo( return ( - + - + diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 7f97c196ee172..26f8d53f1fec2 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -15,7 +15,6 @@ const HISTOGRAM_QUERY_ID = 'authenticationsHistogramQuery'; const AuthenticationsQueryTabBodyComponent: React.FC = ({ deleteQuery, - docValueFields, endDate, filterQuery, indexNames, @@ -45,7 +44,6 @@ const AuthenticationsQueryTabBodyComponent: React.FC startDate={startDate} type={type} skip={skip} - docValueFields={docValueFields} /> ); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx index b72e6572849d1..8e41cad3d5852 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx @@ -17,7 +17,6 @@ const HostsTableManage = manageQuery(HostsTable); export const HostsQueryTabBody = ({ deleteQuery, - docValueFields, endDate, filterQuery, indexNames, @@ -33,7 +32,6 @@ export const HostsQueryTabBody = ({ }, [skip, toggleStatus]); const [loading, { hosts, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }] = useAllHost({ - docValueFields, endDate, filterQuery, indexNames, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/hosts/pages/navigation/types.ts index 0daf3cad34aa8..41e0317676cc8 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/types.ts @@ -13,7 +13,6 @@ import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { HostsTableType, HostsType } from '../../store/model'; import { NavTab } from '../../../common/components/navigation/types'; import { UpdateDateRange } from '../../../common/components/charts/common'; -import { DocValueFields } from '../../../common/containers/source'; export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & HostsTableType.authentications & @@ -35,7 +34,6 @@ export interface QueryTabBodyProps { export type HostsComponentsQueryProps = QueryTabBodyProps & { deleteQuery?: GlobalTimeArgs['deleteQuery']; - docValueFields?: DocValueFields[]; indexNames: string[]; pageFilters?: Filter[]; skip: boolean; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx index f6957fedd83c5..a24806d02d900 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx @@ -17,7 +17,6 @@ const UncommonProcessTableManage = manageQuery(UncommonProcessTable); export const UncommonProcessQueryTabBody = ({ deleteQuery, - docValueFields, endDate, filterQuery, indexNames, @@ -35,7 +34,6 @@ export const UncommonProcessQueryTabBody = ({ loading, { uncommonProcesses, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, ] = useUncommonProcesses({ - docValueFields, endDate, filterQuery, indexNames, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/types.ts b/x-pack/plugins/security_solution/public/hosts/pages/types.ts index 83c23834cc13b..9af7f5b4a20b2 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/types.ts @@ -11,13 +11,11 @@ import { Filter } from '@kbn/es-query'; import { hostsModel } from '../store'; import { GlobalTimeArgs } from '../../common/containers/use_global_time'; import { InputsModelId } from '../../common/store/inputs/constants'; -import { DocValueFields } from '../../common/containers/source'; import { HOSTS_PATH } from '../../../common/constants'; export const hostDetailsPagePath = `${HOSTS_PATH}/:detailName`; export type HostsTabsProps = GlobalTimeArgs & { - docValueFields: DocValueFields[]; filterQuery: string; pageFilters?: Filter[]; indexNames: string[]; 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 161b2aaff4d3e..7f1eeeabfd69a 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 0000000000000..746f2ec5d72a4 --- /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 0000000000000..3e2ae27ffbf09 --- /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 80dd9e9563bb5..d1833c2df0ba2 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/components/policy_response/policy_response.tsx b/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response.tsx index 6fb5cf5b1d814..b468dce0a5b48 100644 --- a/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response.tsx +++ b/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response.tsx @@ -5,129 +5,45 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { memo, useCallback } from 'react'; import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiHealth, EuiText, EuiTreeView, EuiNotificationBadge } from '@elastic/eui'; import { - EuiAccordion, - EuiNotificationBadge, - EuiHealth, - EuiText, - htmlIdGenerator, -} from '@elastic/eui'; -import { + HostPolicyResponseActionStatus, HostPolicyResponseAppliedAction, HostPolicyResponseConfiguration, Immutable, + ImmutableArray, + ImmutableObject, } from '../../../../common/endpoint/types'; -import { POLICY_STATUS_TO_HEALTH_COLOR } from '../../pages/endpoint_hosts/view/host_constants'; import { formatResponse } from './policy_response_friendly_names'; +import { PolicyResponseActionItem } from './policy_response_action_item'; -/** - * Nested accordion in the policy response detailing any concerned - * actions the endpoint took to apply the policy configuration. - */ -const PolicyResponseConfigAccordion = styled(EuiAccordion)` - .euiAccordion__triggerWrapper { - padding: ${(props) => props.theme.eui.paddingSizes.xs}; - } - - &.euiAccordion-isOpen { - background-color: ${(props) => props.theme.eui.euiFocusBackgroundColor}; - } - - .euiAccordion__childWrapper { - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - } - - .policyResponseAttentionBadge { - background-color: ${(props) => props.theme.eui.euiColorDanger}; - color: ${(props) => props.theme.eui.euiColorEmptyShade}; - } - - .euiAccordion__button { - :hover, - :focus { - text-decoration: none; +// Most of them are needed in order to display large react nodes (PolicyResponseActionItem) in child levels. +const StyledEuiTreeView = styled(EuiTreeView)` + .policy-response-action-item-expanded { + height: auto; + .euiTreeView__nodeLabel { + width: 100%; } } - - :hover:not(.euiAccordion-isOpen) { - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - } - - .policyResponseActionsAccordion { - .euiAccordion__iconWrapper, - svg { - height: ${(props) => props.theme.eui.euiIconSizes.small}; - width: ${(props) => props.theme.eui.euiIconSizes.small}; - } - } - .policyResponseStatusHealth { - width: 100px; + padding-top: 5px; } - - .policyResponseMessage { - padding-left: ${(props) => props.theme.eui.paddingSizes.l}; + .euiTreeView__node--expanded { + max-height: none !important; + .policy-response-action-expanded + div { + .euiTreeView__node { + // When response action item displays a callout, this needs to be overwritten to remove the default max height of EuiTreeView + max-height: none !important; + padding-top: ${({ theme }) => theme.eui.paddingSizes.s}; + padding-bottom: ${({ theme }) => theme.eui.paddingSizes.s}; + } + } } `; -const PolicyResponseActions = memo( - ({ - actions, - responseActions, - }: { - actions: Immutable; - responseActions: Immutable; - }) => { - return ( - <> - {actions.map((action, index) => { - const statuses = responseActions.find((responseAction) => responseAction.name === action); - if (statuses === undefined) { - return undefined; - } - return ( - -

{formatResponse(action)}

-
- } - paddingSize="s" - extraAction={ - - -

{formatResponse(statuses.status)}

-
-
- } - > - -

{statuses.message}

-
- - ); - })} - - ); - } -); - -PolicyResponseActions.displayName = 'PolicyResponseActions'; - interface PolicyResponseProps { policyResponseConfig: Immutable; policyResponseActions: Immutable; @@ -143,42 +59,156 @@ export const PolicyResponse = memo( policyResponseActions, policyResponseAttentionCount, }: PolicyResponseProps) => { - const generateId = useMemo(() => htmlIdGenerator(), []); - return ( - <> - {Object.entries(policyResponseConfig).map(([key, val]) => { + const getEntryIcon = useCallback( + (status: HostPolicyResponseActionStatus, unsuccessCounts: number) => + status === HostPolicyResponseActionStatus.success ? ( + + ) : status === HostPolicyResponseActionStatus.unsupported ? ( + + ) : ( + + {unsuccessCounts} + + ), + [] + ); + + const getConcernedActions = useCallback( + (concernedActions: ImmutableArray) => { + return concernedActions.map((actionKey) => { + const action = policyResponseActions.find( + (currentAction) => currentAction.name === actionKey + ) as ImmutableObject; + + return { + label: ( + + {formatResponse(actionKey)} + + ), + id: actionKey, + className: + action.status !== HostPolicyResponseActionStatus.success && + action.status !== HostPolicyResponseActionStatus.unsupported + ? 'policy-response-action-expanded' + : '', + icon: getEntryIcon( + action.status, + action.status !== HostPolicyResponseActionStatus.success ? 1 : 0 + ), + children: [ + { + label: ( + {}} // TODO + /> + ), + id: `action_message_${actionKey}`, + isExpanded: true, + className: + action.status !== HostPolicyResponseActionStatus.success && + action.status !== HostPolicyResponseActionStatus.unsupported + ? 'policy-response-action-item-expanded' + : '', + }, + ], + }; + }); + }, + [getEntryIcon, policyResponseActions] + ); + + const getResponseConfigs = useCallback( + () => + Object.entries(policyResponseConfig).map(([key, val]) => { const attentionCount = policyResponseAttentionCount.get(key); - return ( - -

{formatResponse(key)}

- - } - paddingSize="m" - extraAction={ - attentionCount && - attentionCount > 0 && ( - - {attentionCount} - - ) - } + return { + label: ( + + {formatResponse(key)} + + ), + id: key, + icon: attentionCount ? ( + + {attentionCount} + + ) : ( + + ), + children: getConcernedActions(val.concerned_actions), + }; + }), + [getConcernedActions, policyResponseAttentionCount, policyResponseConfig] + ); + + const generateTreeView = useCallback(() => { + let policyTotalErrors = 0; + for (const count of policyResponseAttentionCount.values()) { + policyTotalErrors += count; + } + return [ + { + label: ( + - -
- ); - })} - + + ), + id: 'policyResponse', + icon: policyTotalErrors ? ( + + {policyTotalErrors} + + ) : undefined, + children: getResponseConfigs(), + }, + ]; + }, [getResponseConfigs, policyResponseAttentionCount]); + + const generatedTreeView = generateTreeView(); + + return ( + ); } ); diff --git a/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_action_item.tsx b/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_action_item.tsx new file mode 100644 index 0000000000000..9f9fdb48ede15 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_action_item.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { EuiButton, EuiCallOut, EuiText, EuiSpacer } from '@elastic/eui'; +import { HostPolicyResponseActionStatus } from '../../../../common/endpoint/types'; + +const StyledEuiCallout = styled(EuiCallOut)` + padding: ${({ theme }) => theme.eui.paddingSizes.s}; + .action-message { + white-space: break-spaces; + text-align: left; + } +`; + +interface PolicyResponseActionItemProps { + status: HostPolicyResponseActionStatus; + actionTitle: string; + actionMessage: string; + actionButtonLabel?: string; + actionButtonOnClick?: () => void; +} +/** + * A policy response action item + */ +export const PolicyResponseActionItem = memo( + ({ + status, + actionTitle, + actionMessage, + actionButtonLabel, + actionButtonOnClick, + }: PolicyResponseActionItemProps) => { + return status !== HostPolicyResponseActionStatus.success && + status !== HostPolicyResponseActionStatus.unsupported ? ( + + + {actionMessage} + + + {actionButtonLabel && actionButtonOnClick && ( + + {actionButtonLabel} + + )} + + ) : ( + + {actionMessage} + + ); + } +); + +PolicyResponseActionItem.displayName = 'PolicyResponseActionItem'; diff --git a/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_wrapper.test.tsx b/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_wrapper.test.tsx index 8979176be36de..1b772f203a0fd 100644 --- a/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_wrapper.test.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; +import userEvent from '@testing-library/user-event'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; -import { PolicyResponseWrapper } from './policy_response_wrapper'; +import { PolicyResponseWrapper, PolicyResponseWrapperProps } from './policy_response_wrapper'; import { HostPolicyResponseActionStatus } from '../../../../common/search_strategy'; import { useGetEndpointPolicyResponse } from '../../hooks/endpoint/use_get_endpoint_policy_response'; import { @@ -72,7 +73,10 @@ describe('when on the policy response', () => { let commonPolicyResponse: HostPolicyResponse; const useGetEndpointPolicyResponseMock = useGetEndpointPolicyResponse as jest.Mock; - let render: () => ReturnType; + let render: ( + props?: Partial + ) => ReturnType; + let renderOpenedTree: () => Promise>; const runMock = (customPolicyResponse?: HostPolicyResponse): void => { commonPolicyResponse = customPolicyResponse ?? createPolicyResponse(); useGetEndpointPolicyResponseMock.mockReturnValue({ @@ -85,57 +89,95 @@ describe('when on the policy response', () => { beforeEach(() => { const mockedContext = createAppRootMockRenderer(); - render = () => mockedContext.render(); + render = (props = {}) => + mockedContext.render(); + renderOpenedTree = async () => { + const component = render(); + userEvent.click(component.getByTestId('endpointPolicyResponseTitle')); + + const configs = component.queryAllByTestId('endpointPolicyResponseConfig'); + for (const config of configs) { + userEvent.click(config); + } + + const actions = component.queryAllByTestId('endpointPolicyResponseAction'); + for (const action of actions) { + userEvent.click(action); + } + return component; + }; }); - it('should include the title', async () => { + it('should include the title as the first tree element', async () => { runMock(); - expect((await render().findByTestId('endpointDetailsPolicyResponseTitle')).textContent).toBe( + expect((await render().findByTestId('endpointPolicyResponseTitle')).textContent).toBe( 'Policy Response' ); }); it('should display timestamp', () => { runMock(); - const timestamp = render().queryByTestId('endpointDetailsPolicyResponseTimestamp'); + const timestamp = render().queryByTestId('endpointPolicyResponseTimestamp'); expect(timestamp).not.toBeNull(); }); - it('should show a configuration section for each protection', async () => { + it('should hide timestamp', () => { runMock(); - const configAccordions = await render().findAllByTestId( - 'endpointDetailsPolicyResponseConfigAccordion' + const timestamp = render({ showRevisionMessage: false }).queryByTestId( + 'endpointPolicyResponseTimestamp' ); - expect(configAccordions).toHaveLength( + expect(timestamp).toBeNull(); + }); + + it('should show a configuration section for each protection', async () => { + runMock(); + const component = await renderOpenedTree(); + + const configTree = await component.findAllByTestId('endpointPolicyResponseConfig'); + expect(configTree).toHaveLength( Object.keys(commonPolicyResponse.Endpoint.policy.applied.response.configurations).length ); }); it('should show an actions section for each configuration', async () => { runMock(); - const actionAccordions = await render().findAllByTestId( - 'endpointDetailsPolicyResponseActionsAccordion' - ); - const action = await render().findAllByTestId('policyResponseAction'); - const statusHealth = await render().findAllByTestId('policyResponseStatusHealth'); - const message = await render().findAllByTestId('policyResponseMessage'); + const component = await renderOpenedTree(); + + const configs = component.queryAllByTestId('endpointPolicyResponseConfig'); + const actions = component.queryAllByTestId('endpointPolicyResponseAction'); + + /* + // Uncomment this when commented tests are fixed. + const statusAttentionHealth = component.queryAllByTestId( + 'endpointPolicyResponseStatusAttentionHealth' + ); + const statusSuccessHealth = component.queryAllByTestId( + 'endpointPolicyResponseStatusSuccessHealth' + ); + const messages = component.queryAllByTestId('endpointPolicyResponseMessage'); + */ let expectedActionAccordionCount = 0; - Object.keys(commonPolicyResponse.Endpoint.policy.applied.response.configurations).forEach( - (key) => { - expectedActionAccordionCount += - commonPolicyResponse.Endpoint.policy.applied.response.configurations[ - key as keyof HostPolicyResponse['Endpoint']['policy']['applied']['response']['configurations'] - ].concerned_actions.length; - } + const configurationKeys = Object.keys( + commonPolicyResponse.Endpoint.policy.applied.response.configurations ); - expect(actionAccordions).toHaveLength(expectedActionAccordionCount); - expect(action).toHaveLength(expectedActionAccordionCount * 2); - expect(statusHealth).toHaveLength(expectedActionAccordionCount * 3); - expect(message).toHaveLength(expectedActionAccordionCount * 4); + configurationKeys.forEach((key) => { + expectedActionAccordionCount += + commonPolicyResponse.Endpoint.policy.applied.response.configurations[ + key as keyof HostPolicyResponse['Endpoint']['policy']['applied']['response']['configurations'] + ].concerned_actions.length; + }); + + expect(configs).toHaveLength(configurationKeys.length); + expect(actions).toHaveLength(expectedActionAccordionCount); + // FIXME: for some reason it is not getting all messages items from DOM even those are rendered. + // expect(messages).toHaveLength(expectedActionAccordionCount); + // expect([...statusSuccessHealth, ...statusAttentionHealth]).toHaveLength( + // expectedActionAccordionCount + configurationKeys.length + 1 + // ); }); - it('should not show any numbered badges if all actions are successful', () => { + it('should not show any numbered badges if all actions are successful', async () => { const policyResponse = createPolicyResponse(HostPolicyResponseActionStatus.success); runMock(policyResponse); @@ -150,8 +192,10 @@ describe('when on the policy response', () => { const policyResponse = createPolicyResponse(HostPolicyResponseActionStatus.failure); runMock(policyResponse); - const attentionBadge = await render().findAllByTestId( - 'endpointDetailsPolicyResponseAttentionBadge' + const component = await renderOpenedTree(); + + const attentionBadge = await component.findAllByTestId( + 'endpointPolicyResponseStatusAttentionHealth' ); expect(attentionBadge).not.toHaveLength(0); }); @@ -160,8 +204,10 @@ describe('when on the policy response', () => { const policyResponse = createPolicyResponse(HostPolicyResponseActionStatus.warning); runMock(policyResponse); - const attentionBadge = await render().findAllByTestId( - 'endpointDetailsPolicyResponseAttentionBadge' + const component = await renderOpenedTree(); + + const attentionBadge = await component.findAllByTestId( + 'endpointPolicyResponseStatusAttentionHealth' ); expect(attentionBadge).not.toHaveLength(0); }); @@ -170,6 +216,7 @@ describe('when on the policy response', () => { const policyResponse = createPolicyResponse(); runMock(policyResponse); - expect(render().getByText('A New Unknown Action')).not.toBeNull(); + const component = await renderOpenedTree(); + expect(component.getByText('A New Unknown Action')).not.toBeNull(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_wrapper.tsx b/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_wrapper.tsx index 0f0c7ac0c0edc..3f30fc5dbb148 100644 --- a/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/management/components/policy_response/policy_response_wrapper.tsx @@ -7,83 +7,99 @@ import React, { memo, useEffect, useState } from 'react'; import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { HostPolicyResponse } from '../../../../common/endpoint/types'; +import type { HostPolicyResponse } from '../../../../common/endpoint/types'; import { PreferenceFormattedDateFromPrimitive } from '../../../common/components/formatted_date'; import { useGetEndpointPolicyResponse } from '../../hooks/endpoint/use_get_endpoint_policy_response'; import { PolicyResponse } from './policy_response'; import { getFailedOrWarningActionCountFromPolicyResponse } from '../../pages/endpoint_hosts/store/utils'; -export const PolicyResponseWrapper = memo<{ +export interface PolicyResponseWrapperProps { endpointId: string; -}>(({ endpointId }) => { - const { data, isLoading, isFetching, isError } = useGetEndpointPolicyResponse(endpointId); + showRevisionMessage?: boolean; + onShowNeedsAttentionBadge?: (val: boolean) => void; +} - const [policyResponseConfig, setPolicyResponseConfig] = - useState(); - const [policyResponseActions, setPolicyResponseActions] = - useState(); - const [policyResponseAttentionCount, setPolicyResponseAttentionCount] = useState< - Map - >(new Map()); +export const PolicyResponseWrapper = memo( + ({ endpointId, showRevisionMessage = true, onShowNeedsAttentionBadge }) => { + const { data, isLoading, isFetching, isError } = useGetEndpointPolicyResponse(endpointId); - useEffect(() => { - if (!!data && !isLoading && !isFetching && !isError) { - setPolicyResponseConfig(data.policy_response.Endpoint.policy.applied.response.configurations); - setPolicyResponseActions(data.policy_response.Endpoint.policy.applied.actions); - setPolicyResponseAttentionCount( - getFailedOrWarningActionCountFromPolicyResponse( - data.policy_response.Endpoint.policy.applied - ) - ); - } - }, [data, isLoading, isFetching, isError]); + const [policyResponseConfig, setPolicyResponseConfig] = + useState(); + const [policyResponseActions, setPolicyResponseActions] = + useState(); + const [policyResponseAttentionCount, setPolicyResponseAttentionCount] = useState< + Map + >(new Map()); - return ( - <> - -

- -

-
- - - - ), - }} - /> - - - {isError && ( - + useEffect(() => { + if (!!data && !isLoading && !isFetching && !isError) { + setPolicyResponseConfig( + data.policy_response.Endpoint.policy.applied.response.configurations + ); + setPolicyResponseActions(data.policy_response.Endpoint.policy.applied.actions); + setPolicyResponseAttentionCount( + getFailedOrWarningActionCountFromPolicyResponse( + data.policy_response.Endpoint.policy.applied + ) + ); + } + }, [data, isLoading, isFetching, isError]); + + // This is needed for the `needs attention` action button in fleet. Will callback `true` if any error in policy response + useEffect(() => { + if (onShowNeedsAttentionBadge) { + for (const count of policyResponseAttentionCount.values()) { + if (count) { + // When an error has found, callback to true and return for loop exit + onShowNeedsAttentionBadge(true); + return; } - /> - )} - {isLoading && } - {policyResponseConfig !== undefined && policyResponseActions !== undefined && ( - - )} - - ); -}); + } + } + }, [policyResponseAttentionCount, onShowNeedsAttentionBadge]); + + return ( + <> + {showRevisionMessage && ( + <> + + + ), + }} + /> + + + + )} + {isError && ( + + } + /> + )} + {isLoading && } + {policyResponseConfig !== undefined && policyResponseActions !== undefined && ( + + )} + + ); + } +); PolicyResponseWrapper.displayName = 'PolicyResponse'; 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 0000000000000..297265953bfed --- /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/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index df13b8ba43f06..28d0a3a91029d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -914,4 +914,26 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ } ), }, + { + key: 'windows.advanced.utilization_limits.cpu', + first_supported_version: '8.3', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.utilization_limits.cpu', + { + defaultMessage: + 'The percentage of the aggregate system CPU to restrict Endpoint to. The range is 20-100%. Anything under 20 gets ignored and causes a policy warning. Default: 100', + } + ), + }, + { + key: 'linux.advanced.utilization_limits.cpu', + first_supported_version: '8.3', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.utilization_limits.cpu', + { + defaultMessage: + 'The percentage of the aggregate system CPU to restrict Endpoint to. The range is 20-100%. Anything under 20 gets ignored and causes a policy warning. Default: 50', + } + ), + }, ]; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx index 20a904844e74e..318dcf0f672e2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx @@ -38,7 +38,7 @@ export const EndpointPolicyCreateExtension = memo

diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_response_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_response_extension.tsx index c971481f0327f..2e952e5332f5c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_response_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_response_extension.tsx @@ -6,24 +6,21 @@ */ import React, { memo } from 'react'; -import styled from 'styled-components'; import { PackagePolicyResponseExtensionComponentProps } from '@kbn/fleet-plugin/public'; import { PolicyResponseWrapper } from '../../../../components/policy_response'; -const Container = styled.div` - padding: ${({ theme }) => theme.eui.paddingSizes.m}; -`; - /** * Exports Endpoint-specific package policy response */ export const EndpointPolicyResponseExtension = memo( - ({ endpointId }) => { + ({ agent, onShowNeedsAttentionBadge }) => { return ( - - - + ); } ); 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 3ea50af79a9c3..6d9ade15971eb 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/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 35ea0dd517e95..200c9810d9fe6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -208,43 +208,7 @@ describe('Body', () => { }); }); }, 20000); - - test('it dispatches the `REMOVE_COLUMN` action when there is a field removed from the custom fields', async () => { - const customFieldId = 'my.custom.runtimeField'; - const { storage } = createSecuritySolutionStorageMock(); - const state: State = { - ...mockGlobalState, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - ...mockGlobalState.timeline.timelineById, - 'timeline-test': { - ...mockGlobalState.timeline.timelineById.test, - id: 'timeline-test', - columns: [ - ...defaultHeaders, - { id: customFieldId, category: 'my', columnHeaderType: 'not-filtered' }, - ], - }, - }, - }, - }; - const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - mount( - - - - ); - - expect(mockDispatch).toBeCalledTimes(1); - expect(mockDispatch).toBeCalledWith({ - payload: { columnId: customFieldId, id: 'timeline-test' }, - type: 'x-pack/timelines/t-grid/REMOVE_COLUMN', - }); - }); }); - describe('action on event', () => { const addaNoteToEvent = (wrapper: ReturnType, note: string) => { wrapper.find('[data-test-subj="add-note"]').first().find('button').simulate('click'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index bf4821e132f05..df4a3703be35f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { noop, isEmpty } from 'lodash/fp'; +import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -147,19 +147,6 @@ export const StatefulBody = React.memo( } }, [isSelectAllChecked, onSelectAll, selectAll]); - useEffect(() => { - if (!isEmpty(browserFields) && !isEmpty(columnHeaders)) { - columnHeaders.forEach(({ id: columnId }) => { - if (browserFields.base?.fields?.[columnId] == null) { - const [category] = columnId.split('.'); - if (browserFields[category]?.fields?.[columnId] == null) { - dispatch(timelineActions.removeColumn({ id, columnId })); - } - } - }); - } - }, [browserFields, columnHeaders, dispatch, id]); - const enabledRowRenderers = useMemo(() => { if ( excludedRowRendererIds && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx index 722f95379257c..8b901e4bd42a2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx @@ -230,5 +230,42 @@ describe('plain_column_renderer', () => { expect(wrapper.find('[data-test-subj="draggableWrapperDiv"]').first().exists()).toBe(true); }); + + test('should join multiple values with a comma [not draggable]', () => { + const data = mockTimelineData[19].data; + const column = plainColumnRenderer.renderColumn({ + columnName: 'process.args', + eventId: _id, + values: getValues('process.args', data), + field: defaultHeaders.find((h) => h.id === 'message')!, + timelineId: 'test', + isDraggable: false, + }); + const wrapper = mount( + + {column} + + ); + const values = getValues('process.args', data); + expect(wrapper.text()).toEqual(values?.join(', ')); + }); + + test('should NOT join multiple values with a comma [draggable]', () => { + const data = mockTimelineData[19].data; + const column = plainColumnRenderer.renderColumn({ + columnName: 'process.args', + eventId: _id, + values: getValues('process.args', data), + field: defaultHeaders.find((h) => h.id === 'message')!, + timelineId: 'test', + isDraggable: true, + }); + const wrapper = mount( + + {column} + + ); + expect(wrapper.find('[data-test-subj="draggableWrapperDiv"]').first().exists()).toBe(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx index a7531a57f4716..b22eeff0b2733 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx @@ -14,7 +14,6 @@ import { TimelineNonEcsData } from '../../../../../../common/search_strategy/tim import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { ColumnRenderer } from './column_renderer'; import { FormattedFieldValue } from './formatted_field'; -import { parseValue } from './parse_value'; export const dataExistsAtColumn = (columnName: string, data: TimelineNonEcsData[]): boolean => data.findIndex((item) => item.field === columnName) !== -1; @@ -43,23 +42,60 @@ export const plainColumnRenderer: ColumnRenderer = { truncate?: boolean; values: string[] | undefined | null; linkValues?: string[] | null | undefined; - }) => - values != null - ? values.map((value, i) => ( - - )) - : getEmptyTagValue(), + }) => { + if (!Array.isArray(values) || values.length === 0) { + return getEmptyTagValue(); + } + // Draggable columns should render individual fields to give the user + // fine-grained control over the individual values + if (isDraggable) { + return values.map((value, i) => ( + + )); + } else { + // In case the column isn't draggable, fields are joined + // to give users a faster overview of all values. + // (note: the filter-related hover actions still produce individual filters for each value) + return ( + + ); + } + }, }; + +function joinValues(values: string[] | undefined | null): string | undefined | null { + if (Array.isArray(values)) { + if (values.length > 0) { + return values.join(', '); + } else { + return values[0]; + } + } + return values; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 085e3bc8b00ce..73c855326a0ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { MockedKeys } from '@kbn/utility-types/jest'; import type { AwaitedProperties } from '@kbn/utility-types'; +import type { MockedKeys } from '@kbn/utility-types-jest'; import type { KibanaRequest } from '@kbn/core/server'; import { coreMock } from '@kbn/core/server/mocks'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index 58a486068013f..1d8d68bd92d0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -136,13 +136,14 @@ export const enrichSignalThreatMatches = async ( ]; const matchedThreats = await getMatchedThreats(matchedThreatIds); - const enrichmentsWithoutAtomic = signalMatches.map((signalMatch) => - buildEnrichments({ + const enrichmentsWithoutAtomic: { [key: string]: ThreatEnrichment[] } = {}; + signalMatches.forEach((signalMatch) => { + enrichmentsWithoutAtomic[signalMatch.signalId] = buildEnrichments({ indicatorPath, queries: signalMatch.queries, threats: matchedThreats, - }) - ); + }); + }); const enrichedSignals: SignalSourceHit[] = uniqueHits.map((signalHit, i) => { const threat = get(signalHit._source, 'threat') ?? {}; @@ -155,7 +156,7 @@ export const enrichSignalThreatMatches = async ( // new issues. const existingEnrichmentValue = get(signalHit._source, 'threat.enrichments') ?? []; const existingEnrichments = [existingEnrichmentValue].flat(); // ensure enrichments is an array - const newEnrichmentsWithoutAtomic = enrichmentsWithoutAtomic[i]; + const newEnrichmentsWithoutAtomic = enrichmentsWithoutAtomic[signalHit._id] ?? []; const newEnrichments = newEnrichmentsWithoutAtomic.map((enrichment) => ({ ...enrichment, matched: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/get_flattened_fields.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/get_flattened_fields.ts new file mode 100644 index 0000000000000..d0a9991f866db --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/get_flattened_fields.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { set } from '@elastic/safer-lodash-set'; +import { get, isEmpty } from 'lodash/fp'; +import { toObjectArrayOfStrings } from '../../../common/utils/to_array'; + +export function getFlattenedFields( + fields: string[], + hitFields: T, + fieldMap: Readonly>, + parentField?: string +) { + return fields.reduce((flattenedFields, fieldName) => { + const fieldPath = `${fieldName}`; + const esField = get(`${parentField ?? ''}['${fieldName}']`, fieldMap); + + if (!isEmpty(esField)) { + const fieldValue = get(`${parentField ?? ''}['${esField}']`, hitFields); + if (!isEmpty(fieldValue)) { + return set( + flattenedFields, + fieldPath, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str) + ); + } + } + + return flattenedFields; + }, {}); +} diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.test.ts index d953cb2979e5c..9dd3ceffdccc7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.test.ts @@ -54,21 +54,16 @@ describe('buildEventEnrichmentQuery', () => { ); }); - it('includes specified docvalue_fields', () => { - const docValueFields = [ - { field: '@timestamp', format: 'date_time' }, - { field: 'event.created', format: 'date_time' }, - { field: 'event.end', format: 'date_time' }, - ]; - const options = buildEventEnrichmentRequestOptionsMock({ docValueFields }); - const query = buildEventEnrichmentQuery(options); - expect(query.body?.docvalue_fields).toEqual(expect.arrayContaining(docValueFields)); - }); - it('requests all fields', () => { const options = buildEventEnrichmentRequestOptionsMock(); const query = buildEventEnrichmentQuery(options); - expect(query.body?.fields).toEqual(['*']); + expect(query.body?.fields).toEqual([ + '*', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ]); }); it('excludes _source', () => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.ts index ea015ea145b3f..6f86b0006d156 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/query.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { isEmpty } from 'lodash'; import { CtiQueries } from '../../../../../../common/search_strategy/security_solution/cti'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { buildIndicatorShouldClauses } from './helpers'; export const buildEventEnrichmentQuery: SecuritySolutionFactory['buildDsl'] = - ({ defaultIndex, docValueFields, eventFields, filterQuery, timerange: { from, to } }) => { + ({ defaultIndex, eventFields, filterQuery, timerange: { from, to } }) => { const filter = [ ...createQueryFilterClauses(filterQuery), { term: { 'event.type': 'indicator' } }, @@ -33,8 +32,13 @@ export const buildEventEnrichmentQuery: SecuritySolutionFactory { allow_no_indices: true, body: { _source: false, - fields: ['*'], + fields: [ + '*', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], query: { bool: { filter: [ diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts index 4f84578118cb6..578b9055ebb89 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts @@ -31,108 +31,6 @@ export const mockOptions: HostsRequestOptions = { 'packetbeat-*', 'winlogbeat-*', ], - docValueFields: [ - { field: '@timestamp', format: 'date_time' }, - { field: 'event.created', format: 'date_time' }, - { field: 'event.end', format: 'date_time' }, - { field: 'event.ingested', format: 'date_time' }, - { field: 'event.start', format: 'date_time' }, - { field: 'file.accessed', format: 'date_time' }, - { field: 'file.created', format: 'date_time' }, - { field: 'file.ctime', format: 'date_time' }, - { field: 'file.mtime', format: 'date_time' }, - { field: 'package.installed', format: 'date_time' }, - { field: 'process.parent.start', format: 'date_time' }, - { field: 'process.start', format: 'date_time' }, - { field: 'system.audit.host.boottime', format: 'date_time' }, - { field: 'system.audit.package.installtime', format: 'date_time' }, - { field: 'system.audit.user.password.last_changed', format: 'date_time' }, - { field: 'tls.client.not_after', format: 'date_time' }, - { field: 'tls.client.not_before', format: 'date_time' }, - { field: 'tls.server.not_after', format: 'date_time' }, - { field: 'tls.server.not_before', format: 'date_time' }, - { field: 'aws.cloudtrail.user_identity.session_context.creation_date', format: 'date_time' }, - { field: 'azure.auditlogs.properties.activity_datetime', format: 'date_time' }, - { field: 'azure.enqueued_time', format: 'date_time' }, - { field: 'azure.signinlogs.properties.created_at', format: 'date_time' }, - { field: 'cef.extensions.agentReceiptTime', format: 'date_time' }, - { field: 'cef.extensions.deviceCustomDate1', format: 'date_time' }, - { field: 'cef.extensions.deviceCustomDate2', format: 'date_time' }, - { field: 'cef.extensions.deviceReceiptTime', format: 'date_time' }, - { field: 'cef.extensions.endTime', format: 'date_time' }, - { field: 'cef.extensions.fileCreateTime', format: 'date_time' }, - { field: 'cef.extensions.fileModificationTime', format: 'date_time' }, - { field: 'cef.extensions.flexDate1', format: 'date_time' }, - { field: 'cef.extensions.managerReceiptTime', format: 'date_time' }, - { field: 'cef.extensions.oldFileCreateTime', format: 'date_time' }, - { field: 'cef.extensions.oldFileModificationTime', format: 'date_time' }, - { field: 'cef.extensions.startTime', format: 'date_time' }, - { field: 'checkpoint.subs_exp', format: 'date_time' }, - { field: 'crowdstrike.event.EndTimestamp', format: 'date_time' }, - { field: 'crowdstrike.event.IncidentEndTime', format: 'date_time' }, - { field: 'crowdstrike.event.IncidentStartTime', format: 'date_time' }, - { field: 'crowdstrike.event.ProcessEndTime', format: 'date_time' }, - { field: 'crowdstrike.event.ProcessStartTime', format: 'date_time' }, - { field: 'crowdstrike.event.StartTimestamp', format: 'date_time' }, - { field: 'crowdstrike.event.Timestamp', format: 'date_time' }, - { field: 'crowdstrike.event.UTCTimestamp', format: 'date_time' }, - { field: 'crowdstrike.metadata.eventCreationTime', format: 'date_time' }, - { field: 'gsuite.admin.email.log_search_filter.end_date', format: 'date_time' }, - { field: 'gsuite.admin.email.log_search_filter.start_date', format: 'date_time' }, - { field: 'gsuite.admin.user.birthdate', format: 'date_time' }, - { field: 'kafka.block_timestamp', format: 'date_time' }, - { field: 'microsoft.defender_atp.lastUpdateTime', format: 'date_time' }, - { field: 'microsoft.defender_atp.resolvedTime', format: 'date_time' }, - { field: 'misp.campaign.first_seen', format: 'date_time' }, - { field: 'misp.campaign.last_seen', format: 'date_time' }, - { field: 'misp.intrusion_set.first_seen', format: 'date_time' }, - { field: 'misp.intrusion_set.last_seen', format: 'date_time' }, - { field: 'misp.observed_data.first_observed', format: 'date_time' }, - { field: 'misp.observed_data.last_observed', format: 'date_time' }, - { field: 'misp.report.published', format: 'date_time' }, - { field: 'misp.threat_indicator.valid_from', format: 'date_time' }, - { field: 'misp.threat_indicator.valid_until', format: 'date_time' }, - { field: 'netflow.collection_time_milliseconds', format: 'date_time' }, - { field: 'netflow.exporter.timestamp', format: 'date_time' }, - { field: 'netflow.flow_end_microseconds', format: 'date_time' }, - { field: 'netflow.flow_end_milliseconds', format: 'date_time' }, - { field: 'netflow.flow_end_nanoseconds', format: 'date_time' }, - { field: 'netflow.flow_end_seconds', format: 'date_time' }, - { field: 'netflow.flow_start_microseconds', format: 'date_time' }, - { field: 'netflow.flow_start_milliseconds', format: 'date_time' }, - { field: 'netflow.flow_start_nanoseconds', format: 'date_time' }, - { field: 'netflow.flow_start_seconds', format: 'date_time' }, - { field: 'netflow.max_export_seconds', format: 'date_time' }, - { field: 'netflow.max_flow_end_microseconds', format: 'date_time' }, - { field: 'netflow.max_flow_end_milliseconds', format: 'date_time' }, - { field: 'netflow.max_flow_end_nanoseconds', format: 'date_time' }, - { field: 'netflow.max_flow_end_seconds', format: 'date_time' }, - { field: 'netflow.min_export_seconds', format: 'date_time' }, - { field: 'netflow.min_flow_start_microseconds', format: 'date_time' }, - { field: 'netflow.min_flow_start_milliseconds', format: 'date_time' }, - { field: 'netflow.min_flow_start_nanoseconds', format: 'date_time' }, - { field: 'netflow.min_flow_start_seconds', format: 'date_time' }, - { field: 'netflow.monitoring_interval_end_milli_seconds', format: 'date_time' }, - { field: 'netflow.monitoring_interval_start_milli_seconds', format: 'date_time' }, - { field: 'netflow.observation_time_microseconds', format: 'date_time' }, - { field: 'netflow.observation_time_milliseconds', format: 'date_time' }, - { field: 'netflow.observation_time_nanoseconds', format: 'date_time' }, - { field: 'netflow.observation_time_seconds', format: 'date_time' }, - { field: 'netflow.system_init_time_milliseconds', format: 'date_time' }, - { field: 'rsa.internal.lc_ctime', format: 'date_time' }, - { field: 'rsa.internal.time', format: 'date_time' }, - { field: 'rsa.time.effective_time', format: 'date_time' }, - { field: 'rsa.time.endtime', format: 'date_time' }, - { field: 'rsa.time.event_queue_time', format: 'date_time' }, - { field: 'rsa.time.event_time', format: 'date_time' }, - { field: 'rsa.time.expire_time', format: 'date_time' }, - { field: 'rsa.time.recorded_time', format: 'date_time' }, - { field: 'rsa.time.stamp', format: 'date_time' }, - { field: 'rsa.time.starttime', format: 'date_time' }, - { field: 'sophos.xg.date', format: 'date_time' }, - { field: 'sophos.xg.eventtime', format: 'date_time' }, - { field: 'sophos.xg.start_time', format: 'date_time' }, - ], factoryQueryType: HostsQueries.hosts, filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, @@ -166,7 +64,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'f6NmWHQBA6bGZw2uJepK', _score: null, - _source: {}, + fields: {}, sort: [1599210921410], }, ], @@ -186,7 +84,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: '4_lmWHQBc39KFIJbFdYv', _score: null, - _source: {}, + fields: {}, sort: [1599210907990], }, ], @@ -206,7 +104,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'z_lmWHQBc39KFIJbAdZP', _score: null, - _source: {}, + fields: {}, sort: [1599210906783], }, ], @@ -226,7 +124,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'uPllWHQBc39KFIJb6tbR', _score: null, - _source: {}, + fields: {}, sort: [1599210900781], }, ], @@ -246,17 +144,11 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '56NlWHQBA6bGZw2uiOfb', _score: null, - _source: { - host: { - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - }, + fields: { + 'host.os.name': 'Windows Server 2019 Datacenter', + 'host.os.family': 'windows', + 'host.os.version': '10.0', + 'host.os.platform': 'windows', }, sort: [1599210880354], }, @@ -277,7 +169,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'FKMwWHQBA6bGZw2uw5Z3', _score: null, - _source: {}, + fields: {}, sort: [1599207421000], }, ], @@ -297,7 +189,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'MKMwWHQBA6bGZw2u0ZZw', _score: null, - _source: {}, + fields: {}, sort: [1599207421000], }, ], @@ -317,17 +209,11 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: '.ds-logs-elastic.agent-default-000001', _id: 'tvTLVHQBc39KFIJb_ykQ', _score: null, - _source: { - host: { - os: { - build: '18362.1016', - kernel: '10.0.18362.1016 (WinBuild.160101.0800)', - name: 'Windows 10 Pro', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - }, + fields: { + 'host.os.name': 'Windows 10 Pro', + 'host.os.family': 'windows', + 'host.os.version': '10.0', + 'host.os.platform': 'windows', }, sort: [1599150487957], }, @@ -348,19 +234,11 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: '.ds-logs-endpoint.events.network-default-000001', _id: 'efTLVHQBc39KFIJbiCgD', _score: null, - _source: { - host: { - os: { - Ext: { variant: 'macOS' }, - kernel: - 'Darwin Kernel Version 18.2.0: Fri Oct 5 19:40:55 PDT 2018; root:xnu-4903.221.2~1/RELEASE_X86_64', - name: 'macOS', - family: 'macos', - version: '10.14.1', - platform: 'macos', - full: 'macOS 10.14.1', - }, - }, + fields: { + 'host.os.name': 'macOS', + 'host.os.family': 'macos', + 'host.os.version': '10.14.1', + 'host.os.platform': 'macos', }, sort: [1599150457515], }, @@ -403,7 +281,7 @@ export const formattedSearchStrategyResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'f6NmWHQBA6bGZw2uJepK', _score: null, - _source: {}, + fields: {}, sort: [1599210921410], }, ], @@ -423,7 +301,7 @@ export const formattedSearchStrategyResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: '4_lmWHQBc39KFIJbFdYv', _score: null, - _source: {}, + fields: {}, sort: [1599210907990], }, ], @@ -443,7 +321,7 @@ export const formattedSearchStrategyResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'z_lmWHQBc39KFIJbAdZP', _score: null, - _source: {}, + fields: {}, sort: [1599210906783], }, ], @@ -463,7 +341,7 @@ export const formattedSearchStrategyResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'uPllWHQBc39KFIJb6tbR', _score: null, - _source: {}, + fields: {}, sort: [1599210900781], }, ], @@ -483,17 +361,11 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '56NlWHQBA6bGZw2uiOfb', _score: null, - _source: { - host: { - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - }, + fields: { + 'host.os.name': 'Windows Server 2019 Datacenter', + 'host.os.family': 'windows', + 'host.os.version': '10.0', + 'host.os.platform': 'windows', }, sort: [1599210880354], }, @@ -514,7 +386,7 @@ export const formattedSearchStrategyResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'FKMwWHQBA6bGZw2uw5Z3', _score: null, - _source: {}, + fields: {}, sort: [1599207421000], }, ], @@ -534,7 +406,7 @@ export const formattedSearchStrategyResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'MKMwWHQBA6bGZw2u0ZZw', _score: null, - _source: {}, + fields: {}, sort: [1599207421000], }, ], @@ -554,17 +426,11 @@ export const formattedSearchStrategyResponse = { _index: '.ds-logs-elastic.agent-default-000001', _id: 'tvTLVHQBc39KFIJb_ykQ', _score: null, - _source: { - host: { - os: { - build: '18362.1016', - kernel: '10.0.18362.1016 (WinBuild.160101.0800)', - name: 'Windows 10 Pro', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - }, + fields: { + 'host.os.name': 'Windows 10 Pro', + 'host.os.family': 'windows', + 'host.os.version': '10.0', + 'host.os.platform': 'windows', }, sort: [1599150487957], }, @@ -585,19 +451,11 @@ export const formattedSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.network-default-000001', _id: 'efTLVHQBc39KFIJbiCgD', _score: null, - _source: { - host: { - os: { - Ext: { variant: 'macOS' }, - kernel: - 'Darwin Kernel Version 18.2.0: Fri Oct 5 19:40:55 PDT 2018; root:xnu-4903.221.2~1/RELEASE_X86_64', - name: 'macOS', - family: 'macos', - version: '10.14.1', - platform: 'macos', - full: 'macOS 10.14.1', - }, - }, + fields: { + 'host.os.name': 'macOS', + 'host.os.family': 'macos', + 'host.os.version': '10.14.1', + 'host.os.platform': 'macos', }, sort: [1599150457515], }, @@ -630,7 +488,6 @@ export const formattedSearchStrategyResponse = { ignore_unavailable: true, track_total_hits: false, body: { - docvalue_fields: mockOptions.docValueFields, aggregations: { host_count: { cardinality: { field: 'host.name' } }, host_data: { @@ -641,7 +498,7 @@ export const formattedSearchStrategyResponse = { top_hits: { size: 1, sort: [{ '@timestamp': { order: 'desc' } }], - _source: { includes: ['host.os.*'] }, + _source: false, }, }, }, @@ -663,6 +520,17 @@ export const formattedSearchStrategyResponse = { ], }, }, + _source: false, + fields: [ + 'host.id', + 'host.name', + 'host.os.name', + 'host.os.version', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 0, }, }, @@ -769,16 +637,11 @@ export const mockBuckets: HostAggEsItem = { _index: 'auditbeat-8.0.0-2019.09.06-000022', _id: 'dl0T_m0BHe9nqdOiF2A8', _score: null, - _source: { - host: { - os: { - kernel: ['5.0.0-1013-gcp'], - name: ['Ubuntu'], - family: ['debian'], - version: ['18.04.2 LTS (Bionic Beaver)'], - platform: ['ubuntu'], - }, - }, + fields: { + 'host.os.name': ['Ubuntu'], + 'host.os.family': ['debian'], + 'host.os.version': ['18.04.2 LTS (Bionic Beaver)'], + 'host.os.platform': ['ubuntu'], }, sort: [1571925726017], }, @@ -798,7 +661,7 @@ export const expectedDsl = { lastSeen: { max: { field: '@timestamp' } }, os: { top_hits: { - _source: { includes: ['host.os.*'] }, + _source: false, size: 1, sort: [{ '@timestamp': { order: 'desc' } }], }, @@ -823,7 +686,17 @@ export const expectedDsl = { ], }, }, - docvalue_fields: mockOptions.docValueFields, + _source: false, + fields: [ + 'host.id', + 'host.name', + 'host.os.name', + 'host.os.version', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 0, }, ignore_unavailable: true, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index bed4a040f92b0..f0d815b332ee6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -6,11 +6,10 @@ */ import { set } from '@elastic/safer-lodash-set/fp'; -import { get, has, head } from 'lodash/fp'; +import { get, has } from 'lodash/fp'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostAggEsItem, - HostBuckets, HostsEdges, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; @@ -57,22 +56,7 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s const aggField = hostFieldsMap[fieldName] ? hostFieldsMap[fieldName].replace(/\./g, '_') : fieldName.replace(/\./g, '_'); - if ( - [ - 'host.ip', - 'host.mac', - 'cloud.instance.id', - 'cloud.machine.type', - 'cloud.provider', - 'cloud.region', - ].includes(fieldName) && - has(aggField, bucket) - ) { - const data: HostBuckets = get(aggField, bucket); - return data.buckets.map((obj) => obj.key); - } else if (has(`${aggField}.buckets`, bucket)) { - return getFirstItem(get(`${aggField}`, bucket)); - } else if (has(aggField, bucket)) { + if (has(aggField, bucket)) { const valueObj: HostValue = get(aggField, bucket); return valueObj.value_as_string; } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { @@ -80,18 +64,10 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s case 'host.name': return get('key', bucket) || null; case 'host.os.name': - return get('os.hits.hits[0]._source.host.os.name', bucket) || null; + return get('os.hits.hits[0].fields["host.os.name"]', bucket) || null; case 'host.os.version': - return get('os.hits.hits[0]._source.host.os.version', bucket) || null; + return get('os.hits.hits[0].fields["host.os.version"]', bucket) || null; } } return null; }; - -const getFirstItem = (data: HostBuckets): string | null => { - const firstItem = head(data.buckets); - if (firstItem == null) { - return null; - } - return firstItem.key; -}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts index 5afe096b6671d..5d650abd14998 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; import type { ISearchRequestParams } from '@kbn/data-plugin/common'; import { Direction, @@ -13,17 +12,21 @@ import { SortField, HostsFields, } from '../../../../../../common/search_strategy'; -import { createQueryFilterClauses } from '../../../../../utils/build_query'; +import { createQueryFilterClauses, reduceFields } from '../../../../../utils/build_query'; import { assertUnreachable } from '../../../../../../common/utility_types'; +import { HOSTS_FIELDS } from './helpers'; +import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; export const buildHostsQuery = ({ defaultIndex, - docValueFields, filterQuery, pagination: { querySize }, sort, timerange: { from, to }, }: HostsRequestOptions): ISearchRequestParams => { + const esFields = reduceFields(HOSTS_FIELDS, { + ...hostFieldsMap, + }); const filter = [ ...createQueryFilterClauses(filterQuery), { @@ -45,7 +48,6 @@ export const buildHostsQuery = ({ ignore_unavailable: true, track_total_hits: false, body: { - ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { ...agg, host_data: { @@ -62,15 +64,21 @@ export const buildHostsQuery = ({ }, }, ], - _source: { - includes: ['host.os.*'], - }, + _source: false, }, }, }, }, }, query: { bool: { filter } }, + _source: false, + fields: [ + ...esFields, + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 0, }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts index 7b8fae9c77fb1..175a0f93d5e07 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts @@ -25,408 +25,6 @@ export const mockOptions: HostDetailsRequestOptions = { 'packetbeat-*', 'winlogbeat-*', ], - docValueFields: [ - { - field: '@timestamp', - format: 'date_time', - }, - { - field: 'event.created', - format: 'date_time', - }, - { - field: 'event.end', - format: 'date_time', - }, - { - field: 'event.ingested', - format: 'date_time', - }, - { - field: 'event.start', - format: 'date_time', - }, - { - field: 'file.accessed', - format: 'date_time', - }, - { - field: 'file.created', - format: 'date_time', - }, - { - field: 'file.ctime', - format: 'date_time', - }, - { - field: 'file.mtime', - format: 'date_time', - }, - { - field: 'package.installed', - format: 'date_time', - }, - { - field: 'process.parent.start', - format: 'date_time', - }, - { - field: 'process.start', - format: 'date_time', - }, - { - field: 'system.audit.host.boottime', - format: 'date_time', - }, - { - field: 'system.audit.package.installtime', - format: 'date_time', - }, - { - field: 'system.audit.user.password.last_changed', - format: 'date_time', - }, - { - field: 'tls.client.not_after', - format: 'date_time', - }, - { - field: 'tls.client.not_before', - format: 'date_time', - }, - { - field: 'tls.server.not_after', - format: 'date_time', - }, - { - field: 'tls.server.not_before', - format: 'date_time', - }, - { - field: 'aws.cloudtrail.user_identity.session_context.creation_date', - format: 'date_time', - }, - { - field: 'azure.auditlogs.properties.activity_datetime', - format: 'date_time', - }, - { - field: 'azure.enqueued_time', - format: 'date_time', - }, - { - field: 'azure.signinlogs.properties.created_at', - format: 'date_time', - }, - { - field: 'cef.extensions.agentReceiptTime', - format: 'date_time', - }, - { - field: 'cef.extensions.deviceCustomDate1', - format: 'date_time', - }, - { - field: 'cef.extensions.deviceCustomDate2', - format: 'date_time', - }, - { - field: 'cef.extensions.deviceReceiptTime', - format: 'date_time', - }, - { - field: 'cef.extensions.endTime', - format: 'date_time', - }, - { - field: 'cef.extensions.fileCreateTime', - format: 'date_time', - }, - { - field: 'cef.extensions.fileModificationTime', - format: 'date_time', - }, - { - field: 'cef.extensions.flexDate1', - format: 'date_time', - }, - { - field: 'cef.extensions.managerReceiptTime', - format: 'date_time', - }, - { - field: 'cef.extensions.oldFileCreateTime', - format: 'date_time', - }, - { - field: 'cef.extensions.oldFileModificationTime', - format: 'date_time', - }, - { - field: 'cef.extensions.startTime', - format: 'date_time', - }, - { - field: 'checkpoint.subs_exp', - format: 'date_time', - }, - { - field: 'crowdstrike.event.EndTimestamp', - format: 'date_time', - }, - { - field: 'crowdstrike.event.IncidentEndTime', - format: 'date_time', - }, - { - field: 'crowdstrike.event.IncidentStartTime', - format: 'date_time', - }, - { - field: 'crowdstrike.event.ProcessEndTime', - format: 'date_time', - }, - { - field: 'crowdstrike.event.ProcessStartTime', - format: 'date_time', - }, - { - field: 'crowdstrike.event.StartTimestamp', - format: 'date_time', - }, - { - field: 'crowdstrike.event.Timestamp', - format: 'date_time', - }, - { - field: 'crowdstrike.event.UTCTimestamp', - format: 'date_time', - }, - { - field: 'crowdstrike.metadata.eventCreationTime', - format: 'date_time', - }, - { - field: 'gsuite.admin.email.log_search_filter.end_date', - format: 'date_time', - }, - { - field: 'gsuite.admin.email.log_search_filter.start_date', - format: 'date_time', - }, - { - field: 'gsuite.admin.user.birthdate', - format: 'date_time', - }, - { - field: 'kafka.block_timestamp', - format: 'date_time', - }, - { - field: 'microsoft.defender_atp.lastUpdateTime', - format: 'date_time', - }, - { - field: 'microsoft.defender_atp.resolvedTime', - format: 'date_time', - }, - { - field: 'misp.campaign.first_seen', - format: 'date_time', - }, - { - field: 'misp.campaign.last_seen', - format: 'date_time', - }, - { - field: 'misp.intrusion_set.first_seen', - format: 'date_time', - }, - { - field: 'misp.intrusion_set.last_seen', - format: 'date_time', - }, - { - field: 'misp.observed_data.first_observed', - format: 'date_time', - }, - { - field: 'misp.observed_data.last_observed', - format: 'date_time', - }, - { - field: 'misp.report.published', - format: 'date_time', - }, - { - field: 'misp.threat_indicator.valid_from', - format: 'date_time', - }, - { - field: 'misp.threat_indicator.valid_until', - format: 'date_time', - }, - { - field: 'netflow.collection_time_milliseconds', - format: 'date_time', - }, - { - field: 'netflow.exporter.timestamp', - format: 'date_time', - }, - { - field: 'netflow.flow_end_microseconds', - format: 'date_time', - }, - { - field: 'netflow.flow_end_milliseconds', - format: 'date_time', - }, - { - field: 'netflow.flow_end_nanoseconds', - format: 'date_time', - }, - { - field: 'netflow.flow_end_seconds', - format: 'date_time', - }, - { - field: 'netflow.flow_start_microseconds', - format: 'date_time', - }, - { - field: 'netflow.flow_start_milliseconds', - format: 'date_time', - }, - { - field: 'netflow.flow_start_nanoseconds', - format: 'date_time', - }, - { - field: 'netflow.flow_start_seconds', - format: 'date_time', - }, - { - field: 'netflow.max_export_seconds', - format: 'date_time', - }, - { - field: 'netflow.max_flow_end_microseconds', - format: 'date_time', - }, - { - field: 'netflow.max_flow_end_milliseconds', - format: 'date_time', - }, - { - field: 'netflow.max_flow_end_nanoseconds', - format: 'date_time', - }, - { - field: 'netflow.max_flow_end_seconds', - format: 'date_time', - }, - { - field: 'netflow.min_export_seconds', - format: 'date_time', - }, - { - field: 'netflow.min_flow_start_microseconds', - format: 'date_time', - }, - { - field: 'netflow.min_flow_start_milliseconds', - format: 'date_time', - }, - { - field: 'netflow.min_flow_start_nanoseconds', - format: 'date_time', - }, - { - field: 'netflow.min_flow_start_seconds', - format: 'date_time', - }, - { - field: 'netflow.monitoring_interval_end_milli_seconds', - format: 'date_time', - }, - { - field: 'netflow.monitoring_interval_start_milli_seconds', - format: 'date_time', - }, - { - field: 'netflow.observation_time_microseconds', - format: 'date_time', - }, - { - field: 'netflow.observation_time_milliseconds', - format: 'date_time', - }, - { - field: 'netflow.observation_time_nanoseconds', - format: 'date_time', - }, - { - field: 'netflow.observation_time_seconds', - format: 'date_time', - }, - { - field: 'netflow.system_init_time_milliseconds', - format: 'date_time', - }, - { - field: 'rsa.internal.lc_ctime', - format: 'date_time', - }, - { - field: 'rsa.internal.time', - format: 'date_time', - }, - { - field: 'rsa.time.effective_time', - format: 'date_time', - }, - { - field: 'rsa.time.endtime', - format: 'date_time', - }, - { - field: 'rsa.time.event_queue_time', - format: 'date_time', - }, - { - field: 'rsa.time.event_time', - format: 'date_time', - }, - { - field: 'rsa.time.expire_time', - format: 'date_time', - }, - { - field: 'rsa.time.recorded_time', - format: 'date_time', - }, - { - field: 'rsa.time.stamp', - format: 'date_time', - }, - { - field: 'rsa.time.starttime', - format: 'date_time', - }, - { - field: 'sophos.xg.date', - format: 'date_time', - }, - { - field: 'sophos.xg.eventtime', - format: 'date_time', - }, - { - field: 'sophos.xg.start_time', - format: 'date_time', - }, - ], factoryQueryType: HostsQueries.details, filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}},{"match_phrase":{"host.name":{"query":"bastion00.siem.estc.dev"}}}],"should":[],"must_not":[]}}', @@ -482,106 +80,21 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'zqY7WXQBA6bGZw2uLeKI', _score: null, - _source: { - process: { - name: 'services.exe', - pid: 564, - executable: 'C:\\Windows\\System32\\services.exe', - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - name: 'siem-windows', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - type: 'winlogbeat', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - version: '8.0.0', - user: { name: 'inside_winlogbeat_user' }, - }, - winlog: { - computer_name: 'siem-windows', - process: { pid: 576, thread: { id: 880 } }, - keywords: ['Audit Success'], - logon: { id: '0x3e7', type: 'Service' }, - channel: 'Security', - event_data: { - LogonGuid: '{00000000-0000-0000-0000-000000000000}', - TargetOutboundDomainName: '-', - VirtualAccount: '%%1843', - LogonType: '5', - IpPort: '-', - TransmittedServices: '-', - SubjectLogonId: '0x3e7', - LmPackageName: '-', - TargetOutboundUserName: '-', - KeyLength: '0', - TargetLogonId: '0x3e7', - RestrictedAdminMode: '-', - SubjectUserName: 'SIEM-WINDOWS$', - TargetLinkedLogonId: '0x0', - ElevatedToken: '%%1842', - SubjectDomainName: 'WORKGROUP', - IpAddress: '-', - ImpersonationLevel: '%%1833', - TargetUserName: 'SYSTEM', - LogonProcessName: 'Advapi ', - TargetDomainName: 'NT AUTHORITY', - SubjectUserSid: 'S-1-5-18', - TargetUserSid: 'S-1-5-18', - AuthenticationPackageName: 'Negotiate', - }, - opcode: 'Info', - version: 2, - record_id: 57818, - task: 'Logon', - event_id: 4624, - provider_guid: '{54849625-5478-4994-a5ba-3e3b0328c30d}', - activity_id: '{d2485217-6bac-0000-8fbb-3f7e2571d601}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Security-Auditing', - }, - log: { level: 'information' }, - source: { domain: '-' }, - message: - 'An account was successfully logged on.\n\nSubject:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSIEM-WINDOWS$\n\tAccount Domain:\t\tWORKGROUP\n\tLogon ID:\t\t0x3E7\n\nLogon Information:\n\tLogon Type:\t\t5\n\tRestricted Admin Mode:\t-\n\tVirtual Account:\t\tNo\n\tElevated Token:\t\tYes\n\nImpersonation Level:\t\tImpersonation\n\nNew Logon:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSYSTEM\n\tAccount Domain:\t\tNT AUTHORITY\n\tLogon ID:\t\t0x3E7\n\tLinked Logon ID:\t\t0x0\n\tNetwork Account Name:\t-\n\tNetwork Account Domain:\t-\n\tLogon GUID:\t\t{00000000-0000-0000-0000-000000000000}\n\nProcess Information:\n\tProcess ID:\t\t0x234\n\tProcess Name:\t\tC:\\Windows\\System32\\services.exe\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t-\n\tSource Port:\t\t-\n\nDetailed Authentication Information:\n\tLogon Process:\t\tAdvapi \n\tAuthentication Package:\tNegotiate\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon session is created. It is generated on the computer that was accessed.\n\nThe subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).\n\nThe New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.\n\nThe network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe impersonation level field indicates the extent to which a process in the logon session can impersonate.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', - cloud: { - availability_zone: 'us-central1-c', - instance: { name: 'siem-windows', id: '9156726559029788564' }, - provider: 'gcp', - machine: { type: 'g1-small' }, - project: { id: 'elastic-siem' }, - }, + fields: { + 'agent.id': ['05e1bff7-d7a8-416a-8554-aa10288fa07d'], + 'host.architecture': ['x86_64'], + 'host.id': ['ce1d3c9b-a815-4643-9641-ada0f2c00609'], + 'host.ip': ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], + 'host.mac': ['42:01:0a:c8:00:0f'], + 'host.name': ['siem-windows'], + 'host.os.family': ['windows'], + 'host.os.name': ['Windows Server 2019 Datacenter'], + 'host.os.platform': ['windows'], + 'host.os.version': ['10.0'], + 'cloud.instance.id': ['9156726559029788564'], + 'cloud.machine.type': ['g1-small'], + 'cloud.provider': ['gcp'], '@timestamp': '2020-09-04T13:08:02.532Z', - related: { user: ['SYSTEM', 'SIEM-WINDOWS$'] }, - ecs: { version: '1.5.0' }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 4624, - provider: 'Microsoft-Windows-Security-Auditing', - created: '2020-09-04T13:08:03.638Z', - kind: 'event', - module: 'security', - action: 'logged-in', - category: 'authentication', - type: 'start', - outcome: 'success', - }, - user: { domain: 'NT AUTHORITY', name: 'SYSTEM', id: 'S-1-5-18' }, }, sort: [1599224882532], }, @@ -605,76 +118,21 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: '.ds-logs-system.auth-default-000001', _id: '9_sfWXQBc39KFIJbIsDh', _score: null, - _source: { - agent: { - hostname: 'siem-kibana', - name: 'siem-kibana', - id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', - ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', - type: 'filebeat', - version: '7.9.1', - }, - process: { name: 'sshd', pid: 20764 }, - log: { file: { path: '/var/log/auth.log' }, offset: 552463 }, - source: { - geo: { - continent_name: 'Europe', - region_iso_code: 'DE-BE', - city_name: 'Berlin', - country_iso_code: 'DE', - region_name: 'Land Berlin', - location: { lon: 13.3512, lat: 52.5727 }, - }, - as: { number: 6805, organization: { name: 'Telefonica Germany' } }, - port: 57457, - ip: '77.183.42.188', - }, - cloud: { - availability_zone: 'us-east1-b', - instance: { name: 'siem-kibana', id: '5412578377715150143' }, - provider: 'gcp', - machine: { type: 'n1-standard-2' }, - project: { id: 'elastic-beats' }, - }, - input: { type: 'log' }, + fields: { + 'agent.id': ['aa3d9dc7-fef1-4c2f-a68d-25785d624e35'], + 'host.architecture': ['x86_64'], + 'host.id': ['aa7ca589f1b8220002f2fc61c64cfbf1'], + 'host.ip': ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + 'host.mac': ['42:01:0a:8e:00:07'], + 'host.name': ['siem-kibana'], + 'host.os.family': ['debian'], + 'host.os.name': ['Debian GNU/Linux'], + 'host.os.platform': ['debian'], + 'host.os.version': ['9 (stretch)'], + 'cloud.instance.id': ['5412578377715150143'], + 'cloud.machine.type': ['n1-standard-2'], + 'cloud.provider': ['gcp'], '@timestamp': '2020-09-04T11:49:21.000Z', - system: { - auth: { - ssh: { - method: 'publickey', - signature: 'RSA SHA256:vv64JNLzKZWYA9vonnGWuW7zxWhyZrL/BFxyIGbISx8', - event: 'Accepted', - }, - }, - }, - ecs: { version: '1.5.0' }, - data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, - host: { - hostname: 'siem-kibana', - os: { - kernel: '4.9.0-8-amd64', - codename: 'stretch', - name: 'Debian GNU/Linux', - family: 'debian', - version: '9 (stretch)', - platform: 'debian', - }, - containerized: false, - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'aa7ca589f1b8220002f2fc61c64cfbf1', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - timezone: '+00:00', - action: 'ssh_login', - type: 'authentication_success', - category: 'authentication', - dataset: 'system.auth', - outcome: 'success', - }, - user: { name: 'tsg' }, }, sort: [1599220161000], }, @@ -697,67 +155,21 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: '.ds-logs-system.auth-default-000001', _id: 'ZfxZWXQBc39KFIJbLN5U', _score: null, - _source: { - agent: { - hostname: 'siem-kibana', - name: 'siem-kibana', - id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', - ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', - type: 'filebeat', - version: '7.9.1', - }, - process: { name: 'sshd', pid: 22913 }, - log: { file: { path: '/var/log/auth.log' }, offset: 562910 }, - source: { - geo: { - continent_name: 'Asia', - region_iso_code: 'KR-28', - city_name: 'Incheon', - country_iso_code: 'KR', - region_name: 'Incheon', - location: { lon: 126.7288, lat: 37.4562 }, - }, - as: { number: 4766, organization: { name: 'Korea Telecom' } }, - ip: '59.15.3.197', - }, - cloud: { - availability_zone: 'us-east1-b', - instance: { name: 'siem-kibana', id: '5412578377715150143' }, - provider: 'gcp', - machine: { type: 'n1-standard-2' }, - project: { id: 'elastic-beats' }, - }, - input: { type: 'log' }, + fields: { + 'agent.id': ['aa3d9dc7-fef1-4c2f-a68d-25785d624e35'], + 'host.architecture': ['x86_64'], + 'host.id': ['aa7ca589f1b8220002f2fc61c64cfbf1'], + 'host.ip': ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + 'host.mac': ['42:01:0a:8e:00:07'], + 'host.name': ['siem-kibana'], + 'host.os.family': ['debian'], + 'host.os.name': ['Debian GNU/Linux'], + 'host.os.platform': ['debian'], + 'host.os.version': ['9 (stretch)'], + 'cloud.instance.id': ['5412578377715150143'], + 'cloud.machine.type': ['n1-standard-2'], + 'cloud.provider': ['gcp'], '@timestamp': '2020-09-04T13:40:46.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, - host: { - hostname: 'siem-kibana', - os: { - kernel: '4.9.0-8-amd64', - codename: 'stretch', - name: 'Debian GNU/Linux', - family: 'debian', - version: '9 (stretch)', - platform: 'debian', - }, - containerized: false, - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'aa7ca589f1b8220002f2fc61c64cfbf1', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - timezone: '+00:00', - action: 'ssh_login', - type: 'authentication_failure', - category: 'authentication', - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'admin' }, }, sort: [1599226846000], }, @@ -784,47 +196,10 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'M_xLWXQBc39KFIJbY7Cb', _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - type: 'filebeat', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 20671 }, - log: { file: { path: '/var/log/auth.log' }, offset: 1028103 }, - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-NY', - city_name: 'New York', - country_iso_code: 'US', - region_name: 'New York', - location: { lon: -74, lat: 40.7157 }, - }, - ip: '64.227.88.245', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, + fields: { + 'agent.id': ['f9a321c1-ec27-49fa-aacf-6a50ef6d836f'], + 'host.name': ['bastion00.siem.estc.dev'], '@timestamp': '2020-09-04T13:25:43.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['64.227.88.245'], user: ['user'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T13:25:47.034172Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'user' }, }, sort: [1599225943000], }, @@ -851,47 +226,10 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'nPxKWXQBc39KFIJb7q4w', _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - type: 'filebeat', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 20665 }, - log: { file: { path: '/var/log/auth.log' }, offset: 1027372 }, - source: { - geo: { - continent_name: 'North America', - region_iso_code: 'US-NY', - city_name: 'New York', - country_iso_code: 'US', - region_name: 'New York', - location: { lon: -74, lat: 40.7157 }, - }, - ip: '64.227.88.245', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, + fields: { + 'agent.id': ['f9a321c1-ec27-49fa-aacf-6a50ef6d836f'], + 'host.name': ['bastion00.siem.estc.dev'], '@timestamp': '2020-09-04T13:25:07.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['64.227.88.245'], user: ['ubuntu'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T13:25:16.974606Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'ubuntu' }, }, sort: [1599225907000], }, @@ -918,67 +256,21 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: '.ds-logs-system.auth-default-000001', _id: 'mPsfWXQBc39KFIJbI8HI', _score: null, - _source: { - agent: { - hostname: 'siem-kibana', - name: 'siem-kibana', - id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', - type: 'filebeat', - ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', - version: '7.9.1', - }, - process: { name: 'sshd', pid: 21506 }, - log: { file: { path: '/var/log/auth.log' }, offset: 556761 }, - source: { - geo: { - continent_name: 'Asia', - region_iso_code: 'IN-DL', - city_name: 'New Delhi', - country_iso_code: 'IN', - region_name: 'National Capital Territory of Delhi', - location: { lon: 77.2245, lat: 28.6358 }, - }, - as: { number: 10029, organization: { name: 'SHYAM SPECTRA PVT LTD' } }, - ip: '180.151.228.166', - }, - cloud: { - availability_zone: 'us-east1-b', - instance: { name: 'siem-kibana', id: '5412578377715150143' }, - provider: 'gcp', - machine: { type: 'n1-standard-2' }, - project: { id: 'elastic-beats' }, - }, - input: { type: 'log' }, + fields: { + 'agent.id': ['aa3d9dc7-fef1-4c2f-a68d-25785d624e35'], + 'host.architecture': ['x86_64'], + 'host.id': ['aa7ca589f1b8220002f2fc61c64cfbf1'], + 'host.ip': ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + 'host.mac': ['42:01:0a:8e:00:07'], + 'host.name': ['siem-kibana'], + 'host.os.family': ['debian'], + 'host.os.name': ['Debian GNU/Linux'], + 'host.os.platform': ['debian'], + 'host.os.version': ['9 (stretch)'], + 'cloud.instance.id': ['5412578377715150143'], + 'cloud.machine.type': ['n1-standard-2'], + 'cloud.provider': ['gcp'], '@timestamp': '2020-09-04T12:26:36.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, - host: { - hostname: 'siem-kibana', - os: { - kernel: '4.9.0-8-amd64', - codename: 'stretch', - name: 'Debian GNU/Linux', - family: 'debian', - version: '9 (stretch)', - platform: 'debian', - }, - containerized: false, - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'aa7ca589f1b8220002f2fc61c64cfbf1', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - timezone: '+00:00', - action: 'ssh_login', - type: 'authentication_failure', - category: 'authentication', - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'odoo' }, }, sort: [1599222396000], }, @@ -1005,48 +297,10 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'aaToWHQBA6bGZw2uR-St', _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - type: 'filebeat', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 20475 }, - log: { file: { path: '/var/log/auth.log' }, offset: 1019218 }, - source: { - geo: { - continent_name: 'Europe', - region_iso_code: 'SE-AB', - city_name: 'Stockholm', - country_iso_code: 'SE', - region_name: 'Stockholm', - location: { lon: 17.7833, lat: 59.25 }, - }, - as: { number: 8473, organization: { name: 'Bahnhof AB' } }, - ip: '178.174.148.58', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, + fields: { + 'agent.id': ['f9a321c1-ec27-49fa-aacf-6a50ef6d836f'], + 'host.name': ['bastion00.siem.estc.dev'], '@timestamp': '2020-09-04T11:37:22.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['178.174.148.58'], user: ['pi'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T11:37:31.797423Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'pi' }, }, sort: [1599219442000], }, @@ -1073,48 +327,10 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'VaP_V3QBA6bGZw2upUbg', _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - type: 'filebeat', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 19849 }, - log: { file: { path: '/var/log/auth.log' }, offset: 981036 }, - source: { - geo: { - continent_name: 'Europe', - country_iso_code: 'HR', - location: { lon: 15.5, lat: 45.1667 }, - }, - as: { - number: 42864, - organization: { name: 'Giganet Internet Szolgaltato Kft' }, - }, - ip: '45.95.168.157', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, + fields: { + 'agent.id': ['f9a321c1-ec27-49fa-aacf-6a50ef6d836f'], + 'host.name': ['bastion00.siem.estc.dev'], '@timestamp': '2020-09-04T07:23:22.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['45.95.168.157'], user: ['demo'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T07:23:26.046346Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'demo' }, }, sort: [1599204202000], }, @@ -1141,72 +357,21 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: '.ds-logs-system.auth-default-000001', _id: 'PqYfWXQBA6bGZw2uIhVU', _score: null, - _source: { - agent: { - hostname: 'siem-kibana', - name: 'siem-kibana', - id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', - ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', - type: 'filebeat', - version: '7.9.1', - }, - process: { name: 'sshd', pid: 20396 }, - log: { file: { path: '/var/log/auth.log' }, offset: 550795 }, - source: { - geo: { - continent_name: 'Asia', - region_iso_code: 'CN-BJ', - city_name: 'Beijing', - country_iso_code: 'CN', - region_name: 'Beijing', - location: { lon: 116.3889, lat: 39.9288 }, - }, - as: { - number: 45090, - organization: { - name: 'Shenzhen Tencent Computer Systems Company Limited', - }, - }, - ip: '123.206.30.76', - }, - cloud: { - availability_zone: 'us-east1-b', - instance: { name: 'siem-kibana', id: '5412578377715150143' }, - provider: 'gcp', - machine: { type: 'n1-standard-2' }, - project: { id: 'elastic-beats' }, - }, - input: { type: 'log' }, + fields: { + 'agent.id': ['aa3d9dc7-fef1-4c2f-a68d-25785d624e35'], + 'host.architecture': ['x86_64'], + 'host.id': ['aa7ca589f1b8220002f2fc61c64cfbf1'], + 'host.ip': ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + 'host.mac': ['42:01:0a:8e:00:07'], + 'host.name': ['siem-kibana'], + 'host.os.family': ['debian'], + 'host.os.name': ['Debian GNU/Linux'], + 'host.os.platform': ['debian'], + 'host.os.version': ['9 (stretch)'], + 'cloud.instance.id': ['5412578377715150143'], + 'cloud.machine.type': ['n1-standard-2'], + 'cloud.provider': ['gcp'], '@timestamp': '2020-09-04T11:20:26.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, - host: { - hostname: 'siem-kibana', - os: { - kernel: '4.9.0-8-amd64', - codename: 'stretch', - name: 'Debian GNU/Linux', - family: 'debian', - version: '9 (stretch)', - platform: 'debian', - }, - containerized: false, - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'aa7ca589f1b8220002f2fc61c64cfbf1', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - timezone: '+00:00', - action: 'ssh_login', - type: 'authentication_failure', - category: 'authentication', - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'git' }, }, sort: [1599218426000], }, @@ -1233,48 +398,10 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _index: 'filebeat-8.0.0-2020.09.02-000001', _id: 'iMABWHQBB-gskclyitP-', _score: null, - _source: { - agent: { - name: 'bastion00.siem.estc.dev', - id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', - type: 'filebeat', - ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', - version: '8.0.0', - }, - process: { name: 'sshd', pid: 19870 }, - log: { file: { path: '/var/log/auth.log' }, offset: 984133 }, - source: { - geo: { - continent_name: 'Europe', - country_iso_code: 'HR', - location: { lon: 15.5, lat: 45.1667 }, - }, - as: { - number: 42864, - organization: { name: 'Giganet Internet Szolgaltato Kft' }, - }, - ip: '45.95.168.157', - }, - fileset: { name: 'auth' }, - input: { type: 'log' }, + fields: { + 'agent.id': ['f9a321c1-ec27-49fa-aacf-6a50ef6d836f'], + 'host.name': ['bastion00.siem.estc.dev'], '@timestamp': '2020-09-04T07:25:28.000Z', - system: { auth: { ssh: { event: 'Invalid' } } }, - ecs: { version: '1.5.0' }, - related: { ip: ['45.95.168.157'], user: ['webadmin'] }, - service: { type: 'system' }, - host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, - event: { - ingested: '2020-09-04T07:25:30.236651Z', - timezone: '+00:00', - kind: 'event', - module: 'system', - action: 'ssh_login', - type: ['authentication_failure', 'info'], - category: ['authentication'], - dataset: 'system.auth', - outcome: 'failure', - }, - user: { name: 'webadmin' }, }, sort: [1599204328000], }, @@ -1403,6 +530,28 @@ export const formattedSearchStrategyResponse = { ], }, }, + _source: false, + fields: [ + 'host.architecture', + 'host.id', + 'host.ip', + 'host.mac', + 'host.name', + 'host.os.family', + 'host.os.name', + 'host.os.platform', + 'host.os.version', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + 'agent.type', + 'agent.id', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 0, }, }, @@ -1676,6 +825,28 @@ export const expectedDsl = { ], }, }, + _source: false, + fields: [ + 'host.architecture', + 'host.id', + 'host.ip', + 'host.mac', + 'host.name', + 'host.os.family', + 'host.os.name', + 'host.os.platform', + 'host.os.version', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + 'agent.type', + 'agent.id', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 0, }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 79abb9d7137e6..4d2f804b3092c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -22,12 +22,11 @@ import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array' import { EndpointAppContext } from '../../../../../endpoint/types'; import { getPendingActionCounts } from '../../../../../endpoint/services'; -export const HOST_FIELDS = [ +export const HOST_DETAILS_FIELDS = [ '_id', 'host.architecture', 'host.id', 'host.ip', - 'host.id', 'host.mac', 'host.name', 'host.os.family', @@ -43,6 +42,7 @@ export const HOST_FIELDS = [ 'endpoint.policyStatus', 'endpoint.sensorVersion', 'agent.type', + 'agent.id', 'endpoint.id', ]; @@ -106,7 +106,7 @@ const getTermsAggregationTypeFromField = (field: string): AggregationRequest => }; export const formatHostItem = (bucket: HostAggEsItem): HostItem => { - return HOST_FIELDS.reduce((flattenedFields, fieldName) => { + return HOST_DETAILS_FIELDS.reduce((flattenedFields, fieldName) => { const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { if (fieldName === '_id') { @@ -127,32 +127,10 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s ? hostFieldsMap[fieldName].replace(/\./g, '_') : fieldName.replace(/\./g, '_'); - if ( - [ - 'host.ip', - 'host.mac', - 'cloud.instance.id', - 'cloud.machine.type', - 'cloud.provider', - 'cloud.region', - ].includes(fieldName) && - has(aggField, bucket) - ) { - const data: HostBuckets = get(aggField, bucket); - return data.buckets.map((obj) => obj.key); - } else if (has(`${aggField}.buckets`, bucket)) { + if (has(`${aggField}.buckets`, bucket)) { return getFirstItem(get(`${aggField}`, bucket)); - } else if (['host.name', 'host.os.name', 'host.os.version', 'endpoint.id'].includes(fieldName)) { - switch (fieldName) { - case 'host.name': - return get('key', bucket) || null; - case 'host.os.name': - return get('os.hits.hits[0]._source.host.os.name', bucket) || null; - case 'host.os.version': - return get('os.hits.hits[0]._source.host.os.version', bucket) || null; - case 'endpoint.id': - return get('endpoint_id.value.buckets[0].key', bucket) || null; - } + } else if (fieldName === 'endpoint.id') { + return get('endpoint_id.value.buckets[0].key', bucket) || null; } else if (has(aggField, bucket)) { const valueObj: HostValue = get(aggField, bucket); return valueObj.value_as_string; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts index 0499d4105f247..b82e264a2e880 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts @@ -48,9 +48,7 @@ export const hostDetails: SecuritySolutionFactory = { const formattedHostItem = formatHostItem(aggregations); const ident = // endpoint-generated ID, NOT elastic-agent-id formattedHostItem.endpoint && formattedHostItem.endpoint.id - ? Array.isArray(formattedHostItem.endpoint.id) - ? formattedHostItem.endpoint.id[0] - : formattedHostItem.endpoint.id + ? formattedHostItem.endpoint.id[0] : null; if (deps == null) { return { ...response, inspect, hostDetails: { ...formattedHostItem } }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts index 33867f78c6542..02d98e255cae6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.ts @@ -9,14 +9,14 @@ import type { ISearchRequestParams } from '@kbn/data-plugin/common'; import { cloudFieldsMap, hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostDetailsRequestOptions } from '../../../../../../common/search_strategy/security_solution'; import { reduceFields } from '../../../../../utils/build_query/reduce_fields'; -import { HOST_FIELDS, buildFieldsTermAggregation } from './helpers'; +import { HOST_DETAILS_FIELDS, buildFieldsTermAggregation } from './helpers'; export const buildHostDetailsQuery = ({ hostName, defaultIndex, timerange: { from, to }, }: HostDetailsRequestOptions): ISearchRequestParams => { - const esFields = reduceFields(HOST_FIELDS, { + const esFields = reduceFields(HOST_DETAILS_FIELDS, { ...hostFieldsMap, ...cloudFieldsMap, }); @@ -58,6 +58,16 @@ export const buildHostDetailsQuery = ({ }, }, query: { bool: { filter } }, + _source: false, + fields: [ + ...esFields, + 'agent.type', + 'agent.id', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 0, }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts index 738a9db683728..a440038baa236 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts @@ -95,6 +95,15 @@ export const buildHostsKpiAuthenticationsQuery = ({ }, }, size: 0, + _source: false, + fields: [ + 'event.outcome', + 'event.category', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts index ed1d0e8edb107..cce45724ae33c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts @@ -57,6 +57,14 @@ export const buildHostsKpiHostsQuery = ({ filter, }, }, + _source: false, + fields: [ + 'host.name', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 0, }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts index c875e23b523e5..0a8be817b9e46 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts @@ -75,6 +75,15 @@ export const buildHostsKpiUniqueIpsQuery = ({ filter, }, }, + _source: false, + fields: [ + 'destination.ip', + 'source.ip', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 0, }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts index 54403f9c392e7..2d3c4b1b46890 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/__mocks__/index.ts @@ -22,7 +22,6 @@ export const mockOptions: HostFirstLastSeenRequestOptions = { 'packetbeat-*', 'winlogbeat-*', ], - docValueFields: [], factoryQueryType: HostsQueries.firstOrLastSeen, hostName: 'siem-kibana', order: Direction.asc, @@ -44,9 +43,6 @@ export const mockSearchStrategyFirstSeenResponse = { _score: 0, _index: 'auditbeat-7.8.0-2021.02.17-000012', _id: 'nRIAs3cBX5UUcOOYANIW', - _source: { - '@timestamp': '2021-02-18T02:37:37.682Z', - }, fields: { '@timestamp': ['2021-02-18T02:37:37.682Z'], }, @@ -76,9 +72,6 @@ export const mockSearchStrategyLastSeenResponse = { _score: 0, _index: 'auditbeat-7.8.0-2021.02.17-000012', _id: 'nRIAs3cBX5UUcOOYANIW', - _source: { - '@timestamp': '2021-02-18T02:37:37.682Z', - }, fields: { '@timestamp': ['2021-02-18T02:37:37.682Z'], }, @@ -107,9 +100,6 @@ export const formattedSearchStrategyFirstResponse = { _index: 'auditbeat-7.8.0-2021.02.17-000012', _id: 'nRIAs3cBX5UUcOOYANIW', _score: 0, - _source: { - '@timestamp': '2021-02-18T02:37:37.682Z', - }, fields: { '@timestamp': ['2021-02-18T02:37:37.682Z'], }, @@ -139,7 +129,13 @@ export const formattedSearchStrategyFirstResponse = { track_total_hits: false, body: { query: { bool: { filter: [{ term: { 'host.name': 'siem-kibana' } }] } }, - _source: ['@timestamp'], + _source: false, + fields: [ + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 1, sort: [ { @@ -173,9 +169,6 @@ export const formattedSearchStrategyLastResponse = { _index: 'auditbeat-7.8.0-2021.02.17-000012', _id: 'nRIAs3cBX5UUcOOYANIW', _score: 0, - _source: { - '@timestamp': '2021-02-18T02:37:37.682Z', - }, fields: { '@timestamp': ['2021-02-18T02:37:37.682Z'], }, @@ -205,7 +198,13 @@ export const formattedSearchStrategyLastResponse = { track_total_hits: false, body: { query: { bool: { filter: [{ term: { 'host.name': 'siem-kibana' } }] } }, - _source: ['@timestamp'], + _source: false, + fields: [ + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 1, sort: [ { @@ -239,7 +238,13 @@ export const expectedDsl = { ignore_unavailable: true, track_total_hits: false, body: { - _source: ['@timestamp'], + _source: false, + fields: [ + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], query: { bool: { filter: [{ term: { 'host.name': 'siem-kibana' } }] } }, size: 1, sort: [{ '@timestamp': { order: Direction.asc } }], diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts index 80393447d62a8..8794a95826a4b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts @@ -30,9 +30,6 @@ export const firstOrLastSeenHost: SecuritySolutionFactory { const filter = [{ term: { 'host.name': hostName } }]; @@ -22,9 +20,14 @@ export const buildFirstOrLastSeenHostQuery = ({ ignore_unavailable: true, track_total_hits: false, body: { - ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), query: { bool: { filter } }, - _source: ['@timestamp'], + _source: false, + fields: [ + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], size: 1, sort: [ { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/__mocks__/index.ts index 8775851bb7e7d..146b904c4c378 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/__mocks__/index.ts @@ -302,6 +302,21 @@ export const formattedSearchStrategyResponse = { }, }, size: 0, + _source: false, + fields: [ + 'host.os.*', + 'event.dataset', + 'event.module', + 'event.category', + 'agent.type', + 'winlog.channel', + 'endgame.event_type_full', + 'network.protocol', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], }, }, null, @@ -515,5 +530,20 @@ export const expectedDsl = { }, }, size: 0, + _source: false, + fields: [ + 'host.os.*', + 'event.dataset', + 'event.module', + 'event.category', + 'agent.type', + 'winlog.channel', + 'endgame.event_type_full', + 'network.protocol', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.overview_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.overview_host.dsl.ts index 1e85fcd9786bb..cbebab5dfcbd9 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.overview_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.overview_host.dsl.ts @@ -290,6 +290,21 @@ export const buildOverviewHostQuery = ({ }, }, size: 0, + _source: false, + fields: [ + 'host.os.*', + 'event.dataset', + 'event.module', + 'event.category', + 'agent.type', + 'winlog.channel', + 'endgame.event_type_full', + 'network.protocol', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], }, } as const; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/__mocks__/index.ts index 2730465323c19..9f67360a0517c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/__mocks__/index.ts @@ -18,7 +18,6 @@ export const mockOptions = { 'packetbeat-*', 'winlogbeat-*', ], - docValueFields: [], factoryQueryType: HostsQueries.uncommonProcesses, filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}},{"match_phrase":{"host.name":{"query":"siem-kibana"}}}],"should":[],"must_not":[]}}', @@ -73,18 +72,14 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'ayrMZnQBB-gskcly0w7l', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', - 'WD', - '/q', - ], - name: 'AM_Delta_Patch_1.323.631.0.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', + 'WD', + '/q', + ], + 'process.name': ['AM_Delta_Patch_1.323.631.0.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599452531834], }, @@ -107,161 +102,16 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'ayrMZnQBB-gskcly0w7l', _score: 0, - _source: { - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - process: { - args: [ - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', - 'WD', - '/q', - ], - parent: { - args: [ - 'C:\\Windows\\system32\\wuauclt.exe', - '/RunHandlerComServer', - ], - name: 'wuauclt.exe', - pid: 4844, - entity_id: '{ce1d3c9b-b573-5f55-b115-000000000b00}', - executable: 'C:\\Windows\\System32\\wuauclt.exe', - command_line: - '"C:\\Windows\\system32\\wuauclt.exe" /RunHandlerComServer', - }, - pe: { - imphash: 'f96ec1e772808eb81774fb67a4ac229e', - }, - name: 'AM_Delta_Patch_1.323.631.0.exe', - pid: 4608, - working_directory: - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\', - entity_id: '{ce1d3c9b-b573-5f55-b215-000000000b00}', - executable: - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', - command_line: - '"C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe" WD /q', - hash: { - sha1: '94eb7f83ddee6942ec5bdb8e218b5bc942158cb3', - sha256: - '562c58193ba7878b396ebc3fb2dccece7ea0d5c6c7d52fc3ac10b62b894260eb', - md5: '5608b911376da958ed93a7f9428ad0b9', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'Microsoft Antimalware WU Stub', - OriginalFileName: 'AM_Delta_Patch_1.323.631.0.exe', - IntegrityLevel: 'System', - TerminalSessionId: '0', - FileVersion: '1.323.673.0', - Product: 'Microsoft Malware Protection', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222529, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 04:22:11.834\nProcessGuid: {ce1d3c9b-b573-5f55-b215-000000000b00}\nProcessId: 4608\nImage: C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe\nFileVersion: 1.323.673.0\nDescription: Microsoft Antimalware WU Stub\nProduct: Microsoft Malware Protection\nCompany: Microsoft Corporation\nOriginalFileName: AM_Delta_Patch_1.323.631.0.exe\nCommandLine: "C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe" WD /q\nCurrentDirectory: C:\\Windows\\SoftwareDistribution\\Download\\Install\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=94EB7F83DDEE6942EC5BDB8E218B5BC942158CB3,MD5=5608B911376DA958ED93A7F9428AD0B9,SHA256=562C58193BA7878B396EBC3FB2DCCECE7EA0D5C6C7D52FC3AC10B62B894260EB,IMPHASH=F96EC1E772808EB81774FB67A4AC229E\nParentProcessGuid: {ce1d3c9b-b573-5f55-b115-000000000b00}\nParentProcessId: 4844\nParentImage: C:\\Windows\\System32\\wuauclt.exe\nParentCommandLine: "C:\\Windows\\system32\\wuauclt.exe" /RunHandlerComServer', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', + 'WD', + '/q', + ], + 'process.name': ['AM_Delta_Patch_1.323.631.0.exe'], '@timestamp': '2020-09-07T04:22:11.834Z', - ecs: { - version: '1.5.0', - }, - related: { - user: 'SYSTEM', - hash: [ - '94eb7f83ddee6942ec5bdb8e218b5bc942158cb3', - '5608b911376da958ed93a7f9428ad0b9', - '562c58193ba7878b396ebc3fb2dccece7ea0d5c6c7d52fc3ac10b62b894260eb', - 'f96ec1e772808eb81774fb67a4ac229e', - ], - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T04:22:12.727Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - type: ['start', 'process_start'], - category: ['process'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: '94eb7f83ddee6942ec5bdb8e218b5bc942158cb3', - imphash: 'f96ec1e772808eb81774fb67a4ac229e', - sha256: - '562c58193ba7878b396ebc3fb2dccece7ea0d5c6c7d52fc3ac10b62b894260eb', - md5: '5608b911376da958ed93a7f9428ad0b9', - }, + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], }, }, ], @@ -286,18 +136,15 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'M-GvaHQBA6bGZw2uBoYz', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', - 'WD', - '/q', - ], - name: 'AM_Delta_Patch_1.323.673.0.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', + 'WD', + '/q', + ], + 'process.name': ['AM_Delta_Patch_1.323.673.0.exe'], + '@timestamp': '2020-09-07T04:22:11.834Z', + 'user.name': ['SYSTEM'], }, sort: [1599484132366], }, @@ -320,161 +167,16 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'M-GvaHQBA6bGZw2uBoYz', _score: 0, - _source: { - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - name: 'siem-windows', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - process: { - args: [ - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', - 'WD', - '/q', - ], - parent: { - args: [ - 'C:\\Windows\\system32\\wuauclt.exe', - '/RunHandlerComServer', - ], - name: 'wuauclt.exe', - pid: 4548, - entity_id: '{ce1d3c9b-30e3-5f56-ca15-000000000b00}', - executable: 'C:\\Windows\\System32\\wuauclt.exe', - command_line: - '"C:\\Windows\\system32\\wuauclt.exe" /RunHandlerComServer', - }, - pe: { - imphash: 'f96ec1e772808eb81774fb67a4ac229e', - }, - name: 'AM_Delta_Patch_1.323.673.0.exe', - working_directory: - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\', - pid: 4684, - entity_id: '{ce1d3c9b-30e4-5f56-cb15-000000000b00}', - executable: - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', - command_line: - '"C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe" WD /q', - hash: { - sha1: 'ae1e653f1e53dcd34415a35335f9e44d2a33be65', - sha256: - '4382c96613850568d003c02ba0a285f6d2ef9b8c20790ffa2b35641bc831293f', - md5: 'd088fcf98bb9aa1e8f07a36b05011555', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'Microsoft Antimalware WU Stub', - OriginalFileName: 'AM_Delta_Patch_1.323.673.0.exe', - IntegrityLevel: 'System', - TerminalSessionId: '0', - FileVersion: '1.323.693.0', - Product: 'Microsoft Malware Protection', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 223146, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 13:08:52.366\nProcessGuid: {ce1d3c9b-30e4-5f56-cb15-000000000b00}\nProcessId: 4684\nImage: C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe\nFileVersion: 1.323.693.0\nDescription: Microsoft Antimalware WU Stub\nProduct: Microsoft Malware Protection\nCompany: Microsoft Corporation\nOriginalFileName: AM_Delta_Patch_1.323.673.0.exe\nCommandLine: "C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe" WD /q\nCurrentDirectory: C:\\Windows\\SoftwareDistribution\\Download\\Install\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=AE1E653F1E53DCD34415A35335F9E44D2A33BE65,MD5=D088FCF98BB9AA1E8F07A36B05011555,SHA256=4382C96613850568D003C02BA0A285F6D2EF9B8C20790FFA2B35641BC831293F,IMPHASH=F96EC1E772808EB81774FB67A4AC229E\nParentProcessGuid: {ce1d3c9b-30e3-5f56-ca15-000000000b00}\nParentProcessId: 4548\nParentImage: C:\\Windows\\System32\\wuauclt.exe\nParentCommandLine: "C:\\Windows\\system32\\wuauclt.exe" /RunHandlerComServer', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', + 'WD', + '/q', + ], + 'process.name': ['AM_Delta_Patch_1.323.673.0.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T13:08:52.366Z', - ecs: { - version: '1.5.0', - }, - related: { - user: 'SYSTEM', - hash: [ - 'ae1e653f1e53dcd34415a35335f9e44d2a33be65', - 'd088fcf98bb9aa1e8f07a36b05011555', - '4382c96613850568d003c02ba0a285f6d2ef9b8c20790ffa2b35641bc831293f', - 'f96ec1e772808eb81774fb67a4ac229e', - ], - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T13:08:53.889Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: 'ae1e653f1e53dcd34415a35335f9e44d2a33be65', - imphash: 'f96ec1e772808eb81774fb67a4ac229e', - sha256: - '4382c96613850568d003c02ba0a285f6d2ef9b8c20790ffa2b35641bc831293f', - md5: 'd088fcf98bb9aa1e8f07a36b05011555', - }, }, }, ], @@ -499,14 +201,10 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'cinEZnQBB-gskclyvNmU', _score: null, - _source: { - process: { - args: ['C:\\Windows\\system32\\devicecensus.exe'], - name: 'DeviceCensus.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\devicecensus.exe'], + 'process.name': ['DeviceCensus.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599452000791], }, @@ -529,150 +227,12 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'cinEZnQBB-gskclyvNmU', _score: 0, - _source: { - process: { - args: ['C:\\Windows\\system32\\devicecensus.exe'], - parent: { - args: ['C:\\Windows\\system32\\svchost.exe', '-k', 'netsvcs', '-p'], - name: 'svchost.exe', - pid: 1060, - entity_id: '{ce1d3c9b-b9b1-5f34-1c00-000000000b00}', - executable: 'C:\\Windows\\System32\\svchost.exe', - command_line: 'C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - }, - pe: { - imphash: '0cdb6b589f0a125609d8df646de0ea86', - }, - name: 'DeviceCensus.exe', - pid: 5016, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-b360-5f55-a115-000000000b00}', - executable: 'C:\\Windows\\System32\\DeviceCensus.exe', - command_line: 'C:\\Windows\\system32\\devicecensus.exe', - hash: { - sha1: '9e488437b2233e5ad9abd3151ec28ea51eb64c2d', - sha256: - 'dbea7473d5e7b3b4948081dacc6e35327d5a588f4fd0a2d68184bffd10439296', - md5: '8159944c79034d2bcabf73d461a7e643', - }, - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - name: 'siem-windows', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - Description: 'Device Census', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - OriginalFileName: 'DeviceCensus.exe', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.18362.1035 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222507, - task: 'Process Create (rule: ProcessCreate)', - event_id: 1, - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 04:13:20.791\nProcessGuid: {ce1d3c9b-b360-5f55-a115-000000000b00}\nProcessId: 5016\nImage: C:\\Windows\\System32\\DeviceCensus.exe\nFileVersion: 10.0.18362.1035 (WinBuild.160101.0800)\nDescription: Device Census\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: DeviceCensus.exe\nCommandLine: C:\\Windows\\system32\\devicecensus.exe\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=9E488437B2233E5AD9ABD3151EC28EA51EB64C2D,MD5=8159944C79034D2BCABF73D461A7E643,SHA256=DBEA7473D5E7B3B4948081DACC6E35327D5A588F4FD0A2D68184BFFD10439296,IMPHASH=0CDB6B589F0A125609D8DF646DE0EA86\nParentProcessGuid: {ce1d3c9b-b9b1-5f34-1c00-000000000b00}\nParentProcessId: 1060\nParentImage: C:\\Windows\\System32\\svchost.exe\nParentCommandLine: C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\devicecensus.exe'], + 'process.name': ['DeviceCensus.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T04:13:20.791Z', - related: { - user: 'SYSTEM', - hash: [ - '9e488437b2233e5ad9abd3151ec28ea51eb64c2d', - '8159944c79034d2bcabf73d461a7e643', - 'dbea7473d5e7b3b4948081dacc6e35327d5a588f4fd0a2d68184bffd10439296', - '0cdb6b589f0a125609d8df646de0ea86', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T04:13:22.458Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: '9e488437b2233e5ad9abd3151ec28ea51eb64c2d', - imphash: '0cdb6b589f0a125609d8df646de0ea86', - sha256: - 'dbea7473d5e7b3b4948081dacc6e35327d5a588f4fd0a2d68184bffd10439296', - md5: '8159944c79034d2bcabf73d461a7e643', - }, }, }, ], @@ -697,14 +257,10 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'HNKSZHQBA6bGZw2uCtRk', _score: null, - _source: { - process: { - args: ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], - name: 'DiskSnapshot.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], + 'process.name': ['DiskSnapshot.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599415124040], }, @@ -727,150 +283,12 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'HNKSZHQBA6bGZw2uCtRk', _score: 0, - _source: { - process: { - args: ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], - parent: { - args: ['C:\\Windows\\system32\\svchost.exe', '-k', 'netsvcs', '-p'], - name: 'svchost.exe', - pid: 1060, - entity_id: '{ce1d3c9b-b9b1-5f34-1c00-000000000b00}', - executable: 'C:\\Windows\\System32\\svchost.exe', - command_line: 'C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - }, - pe: { - imphash: '69bdabb73b409f40ad05f057cec29380', - }, - name: 'DiskSnapshot.exe', - pid: 3120, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-2354-5f55-6415-000000000b00}', - command_line: 'C:\\Windows\\system32\\disksnapshot.exe -z', - executable: 'C:\\Windows\\System32\\DiskSnapshot.exe', - hash: { - sha1: '61b4d8d4757e15259e1e92c8236f37237b5380d1', - sha256: - 'c7b9591eb4dd78286615401c138c7c1a89f0e358caae1786de2c3b08e904ffdc', - md5: 'ece311ff51bd847a3874bfac85449c6b', - }, - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'DiskSnapshot.exe', - OriginalFileName: 'DiskSnapshot.exe', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.17763.652 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 221799, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-06 17:58:44.040\nProcessGuid: {ce1d3c9b-2354-5f55-6415-000000000b00}\nProcessId: 3120\nImage: C:\\Windows\\System32\\DiskSnapshot.exe\nFileVersion: 10.0.17763.652 (WinBuild.160101.0800)\nDescription: DiskSnapshot.exe\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: DiskSnapshot.exe\nCommandLine: C:\\Windows\\system32\\disksnapshot.exe -z\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=61B4D8D4757E15259E1E92C8236F37237B5380D1,MD5=ECE311FF51BD847A3874BFAC85449C6B,SHA256=C7B9591EB4DD78286615401C138C7C1A89F0E358CAAE1786DE2C3B08E904FFDC,IMPHASH=69BDABB73B409F40AD05F057CEC29380\nParentProcessGuid: {ce1d3c9b-b9b1-5f34-1c00-000000000b00}\nParentProcessId: 1060\nParentImage: C:\\Windows\\System32\\svchost.exe\nParentCommandLine: C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], + 'process.name': ['DiskSnapshot.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-06T17:58:44.040Z', - related: { - user: 'SYSTEM', - hash: [ - '61b4d8d4757e15259e1e92c8236f37237b5380d1', - 'ece311ff51bd847a3874bfac85449c6b', - 'c7b9591eb4dd78286615401c138c7c1a89f0e358caae1786de2c3b08e904ffdc', - '69bdabb73b409f40ad05f057cec29380', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-06T17:58:45.606Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: '61b4d8d4757e15259e1e92c8236f37237b5380d1', - imphash: '69bdabb73b409f40ad05f057cec29380', - sha256: - 'c7b9591eb4dd78286615401c138c7c1a89f0e358caae1786de2c3b08e904ffdc', - md5: 'ece311ff51bd847a3874bfac85449c6b', - }, }, }, ], @@ -895,17 +313,13 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '2zncaHQBB-gskcly1QaD', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', - '{6BB79B50-2038-4A10-B513-2FAC72FF213E}', - ], - name: 'DismHost.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', + '{6BB79B50-2038-4A10-B513-2FAC72FF213E}', + ], + 'process.name': ['DismHost.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599487135371], }, @@ -928,159 +342,15 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '2zncaHQBB-gskcly1QaD', _score: 0, - _source: { - process: { - args: [ - 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', - '{6BB79B50-2038-4A10-B513-2FAC72FF213E}', - ], - parent: { - args: [ - 'C:\\ProgramData\\Microsoft\\Windows Defender\\platform\\4.18.2008.9-0\\MsMpEng.exe', - ], - name: 'MsMpEng.exe', - pid: 184, - entity_id: '{ce1d3c9b-1b55-5f4f-4913-000000000b00}', - executable: - 'C:\\ProgramData\\Microsoft\\Windows Defender\\Platform\\4.18.2008.9-0\\MsMpEng.exe', - command_line: - '"C:\\ProgramData\\Microsoft\\Windows Defender\\platform\\4.18.2008.9-0\\MsMpEng.exe"', - }, - pe: { - imphash: 'a644b5814b05375757429dfb05524479', - }, - name: 'DismHost.exe', - pid: 1500, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-3c9f-5f56-d315-000000000b00}', - executable: - 'C:\\Windows\\Temp\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\DismHost.exe', - command_line: - 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe {6BB79B50-2038-4A10-B513-2FAC72FF213E}', - hash: { - sha1: 'a8a65b6a45a988f06e17ebd04e5462ca730d2337', - sha256: - 'b94317b7c665f1cec965e3322e0aa26c8be29eaf5830fb7fcd7e14ae88a8cf22', - md5: '5867dc628a444f2393f7eff007bd4417', - }, - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - name: 'siem-windows', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - type: 'winlogbeat', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'Dism Host Servicing Process', - OriginalFileName: 'DismHost.exe', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.17763.771 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 223274, - task: 'Process Create (rule: ProcessCreate)', - event_id: 1, - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 13:58:55.371\nProcessGuid: {ce1d3c9b-3c9f-5f56-d315-000000000b00}\nProcessId: 1500\nImage: C:\\Windows\\Temp\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\DismHost.exe\nFileVersion: 10.0.17763.771 (WinBuild.160101.0800)\nDescription: Dism Host Servicing Process\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: DismHost.exe\nCommandLine: C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe {6BB79B50-2038-4A10-B513-2FAC72FF213E}\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=A8A65B6A45A988F06E17EBD04E5462CA730D2337,MD5=5867DC628A444F2393F7EFF007BD4417,SHA256=B94317B7C665F1CEC965E3322E0AA26C8BE29EAF5830FB7FCD7E14AE88A8CF22,IMPHASH=A644B5814B05375757429DFB05524479\nParentProcessGuid: {ce1d3c9b-1b55-5f4f-4913-000000000b00}\nParentProcessId: 184\nParentImage: C:\\ProgramData\\Microsoft\\Windows Defender\\Platform\\4.18.2008.9-0\\MsMpEng.exe\nParentCommandLine: "C:\\ProgramData\\Microsoft\\Windows Defender\\platform\\4.18.2008.9-0\\MsMpEng.exe"', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', + '{6BB79B50-2038-4A10-B513-2FAC72FF213E}', + ], + 'process.name': ['DismHost.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T13:58:55.371Z', - related: { - user: 'SYSTEM', - hash: [ - 'a8a65b6a45a988f06e17ebd04e5462ca730d2337', - '5867dc628a444f2393f7eff007bd4417', - 'b94317b7c665f1cec965e3322e0aa26c8be29eaf5830fb7fcd7e14ae88a8cf22', - 'a644b5814b05375757429dfb05524479', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T13:58:56.138Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: 'a8a65b6a45a988f06e17ebd04e5462ca730d2337', - imphash: 'a644b5814b05375757429dfb05524479', - sha256: - 'b94317b7c665f1cec965e3322e0aa26c8be29eaf5830fb7fcd7e14ae88a8cf22', - md5: '5867dc628a444f2393f7eff007bd4417', - }, }, }, ], @@ -1105,18 +375,14 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'gdVuZXQBA6bGZw2uFsPP', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\System32\\sihclient.exe', - '/cv', - '33nfV21X50ie84HvATAt1w.0.1', - ], - name: 'SIHClient.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\System32\\sihclient.exe', + '/cv', + '33nfV21X50ie84HvATAt1w.0.1', + ], + 'process.name': ['SIHClient.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599429545370], }, @@ -1139,162 +405,16 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'gdVuZXQBA6bGZw2uFsPP', _score: 0, - _source: { - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - process: { - args: [ - 'C:\\Windows\\System32\\sihclient.exe', - '/cv', - '33nfV21X50ie84HvATAt1w.0.1', - ], - parent: { - args: [ - 'C:\\Windows\\System32\\Upfc.exe', - '/launchtype', - 'periodic', - '/cv', - '33nfV21X50ie84HvATAt1w.0', - ], - name: 'upfc.exe', - pid: 4328, - entity_id: '{ce1d3c9b-5b8b-5f55-7815-000000000b00}', - executable: 'C:\\Windows\\System32\\upfc.exe', - command_line: - 'C:\\Windows\\System32\\Upfc.exe /launchtype periodic /cv 33nfV21X50ie84HvATAt1w.0', - }, - pe: { - imphash: '3bbd1eea2778ee3dcd883a4d5533aec3', - }, - name: 'SIHClient.exe', - pid: 2780, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-5ba9-5f55-8815-000000000b00}', - executable: 'C:\\Windows\\System32\\SIHClient.exe', - command_line: - 'C:\\Windows\\System32\\sihclient.exe /cv 33nfV21X50ie84HvATAt1w.0.1', - hash: { - sha1: '145ef8d82cf1e451381584cd9565a2d35a442504', - sha256: - '0e0bb70ae1888060b3ffb9a320963551b56dd0d4ce0b5dc1c8fadda4b7bf3f6a', - md5: 'dc1e380b36f4a8309f363d3809e607b8', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'SIH Client', - OriginalFileName: 'sihclient.exe', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.17763.1217 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222106, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-06 21:59:05.370\nProcessGuid: {ce1d3c9b-5ba9-5f55-8815-000000000b00}\nProcessId: 2780\nImage: C:\\Windows\\System32\\SIHClient.exe\nFileVersion: 10.0.17763.1217 (WinBuild.160101.0800)\nDescription: SIH Client\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: sihclient.exe\nCommandLine: C:\\Windows\\System32\\sihclient.exe /cv 33nfV21X50ie84HvATAt1w.0.1\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=145EF8D82CF1E451381584CD9565A2D35A442504,MD5=DC1E380B36F4A8309F363D3809E607B8,SHA256=0E0BB70AE1888060B3FFB9A320963551B56DD0D4CE0B5DC1C8FADDA4B7BF3F6A,IMPHASH=3BBD1EEA2778EE3DCD883A4D5533AEC3\nParentProcessGuid: {ce1d3c9b-5b8b-5f55-7815-000000000b00}\nParentProcessId: 4328\nParentImage: C:\\Windows\\System32\\upfc.exe\nParentCommandLine: C:\\Windows\\System32\\Upfc.exe /launchtype periodic /cv 33nfV21X50ie84HvATAt1w.0', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\System32\\sihclient.exe', + '/cv', + '33nfV21X50ie84HvATAt1w.0.1', + ], + 'process.name': ['SIHClient.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-06T21:59:05.370Z', - related: { - user: 'SYSTEM', - hash: [ - '145ef8d82cf1e451381584cd9565a2d35a442504', - 'dc1e380b36f4a8309f363d3809e607b8', - '0e0bb70ae1888060b3ffb9a320963551b56dd0d4ce0b5dc1c8fadda4b7bf3f6a', - '3bbd1eea2778ee3dcd883a4d5533aec3', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - kind: 'event', - created: '2020-09-06T21:59:06.713Z', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: '145ef8d82cf1e451381584cd9565a2d35a442504', - imphash: '3bbd1eea2778ee3dcd883a4d5533aec3', - sha256: - '0e0bb70ae1888060b3ffb9a320963551b56dd0d4ce0b5dc1c8fadda4b7bf3f6a', - md5: 'dc1e380b36f4a8309f363d3809e607b8', - }, }, }, ], @@ -1319,16 +439,12 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '6NmKZnQBA6bGZw2uma12', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', - ], - name: 'SpeechModelDownload.exe', - }, - user: { - name: 'NETWORK SERVICE', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', + ], + 'process.name': ['SpeechModelDownload.exe'], + 'user.name': ['NETWORK SERVICE'], }, sort: [1599448191225], }, @@ -1351,154 +467,14 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '6NmKZnQBA6bGZw2uma12', _score: 0, - _source: { - process: { - args: [ - 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', - ], - parent: { - args: ['C:\\Windows\\system32\\svchost.exe', '-k', 'netsvcs', '-p'], - name: 'svchost.exe', - pid: 1060, - entity_id: '{ce1d3c9b-b9b1-5f34-1c00-000000000b00}', - executable: 'C:\\Windows\\System32\\svchost.exe', - command_line: 'C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - }, - pe: { - imphash: '23bd5f904494d14029d9263cebae088d', - }, - name: 'SpeechModelDownload.exe', - working_directory: 'C:\\Windows\\system32\\', - pid: 4328, - entity_id: '{ce1d3c9b-a47f-5f55-9915-000000000b00}', - hash: { - sha1: '03e6e81192621dfd873814de3787c6e7d6af1509', - sha256: - '963fd9dc1b82c44d00eb91d61e2cb442af7357e3a603c23d469df53a6376f073', - md5: '3fd687e97e03d303e02bb37ec85de962', - }, - executable: - 'C:\\Windows\\System32\\Speech_OneCore\\common\\SpeechModelDownload.exe', - command_line: - 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9ac-5f34-e403-000000000000}', - Description: 'Speech Model Download Executable', - OriginalFileName: 'SpeechModelDownload.exe', - IntegrityLevel: 'System', - TerminalSessionId: '0', - FileVersion: '10.0.17763.1369 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e4', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222431, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 03:09:51.225\nProcessGuid: {ce1d3c9b-a47f-5f55-9915-000000000b00}\nProcessId: 4328\nImage: C:\\Windows\\System32\\Speech_OneCore\\common\\SpeechModelDownload.exe\nFileVersion: 10.0.17763.1369 (WinBuild.160101.0800)\nDescription: Speech Model Download Executable\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: SpeechModelDownload.exe\nCommandLine: C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\NETWORK SERVICE\nLogonGuid: {ce1d3c9b-b9ac-5f34-e403-000000000000}\nLogonId: 0x3E4\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=03E6E81192621DFD873814DE3787C6E7D6AF1509,MD5=3FD687E97E03D303E02BB37EC85DE962,SHA256=963FD9DC1B82C44D00EB91D61E2CB442AF7357E3A603C23D469DF53A6376F073,IMPHASH=23BD5F904494D14029D9263CEBAE088D\nParentProcessGuid: {ce1d3c9b-b9b1-5f34-1c00-000000000b00}\nParentProcessId: 1060\nParentImage: C:\\Windows\\System32\\svchost.exe\nParentCommandLine: C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', + ], + 'process.name': ['SpeechModelDownload.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['NETWORK SERVICE'], '@timestamp': '2020-09-07T03:09:51.225Z', - related: { - user: 'NETWORK SERVICE', - hash: [ - '03e6e81192621dfd873814de3787c6e7d6af1509', - '3fd687e97e03d303e02bb37ec85de962', - '963fd9dc1b82c44d00eb91d61e2cb442af7357e3a603c23d469df53a6376f073', - '23bd5f904494d14029d9263cebae088d', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - kind: 'event', - created: '2020-09-07T03:09:52.370Z', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - type: ['start', 'process_start'], - category: ['process'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'NETWORK SERVICE', - }, - hash: { - sha1: '03e6e81192621dfd873814de3787c6e7d6af1509', - imphash: '23bd5f904494d14029d9263cebae088d', - sha256: - '963fd9dc1b82c44d00eb91d61e2cb442af7357e3a603c23d469df53a6376f073', - md5: '3fd687e97e03d303e02bb37ec85de962', - }, }, }, ], @@ -1523,14 +499,10 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'Pi68Z3QBc39KFIJb3txa', _score: null, - _source: { - process: { - args: ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], - name: 'UsoClient.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], + 'process.name': ['UsoClient.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599468262455], }, @@ -1553,150 +525,12 @@ export const mockSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'Pi68Z3QBc39KFIJb3txa', _score: 0, - _source: { - process: { - args: ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], - parent: { - args: ['C:\\Windows\\system32\\svchost.exe', '-k', 'netsvcs', '-p'], - name: 'svchost.exe', - pid: 1060, - entity_id: '{ce1d3c9b-b9b1-5f34-1c00-000000000b00}', - executable: 'C:\\Windows\\System32\\svchost.exe', - command_line: 'C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - }, - pe: { - imphash: '2510e8a4554aef2caf0a913be015929f', - }, - name: 'UsoClient.exe', - pid: 3864, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-f2e6-5f55-bc15-000000000b00}', - command_line: 'C:\\Windows\\system32\\usoclient.exe StartScan', - executable: 'C:\\Windows\\System32\\UsoClient.exe', - hash: { - sha1: 'ebf56ad89d4740359d5d3d5370b31e56614bbb79', - sha256: - 'df3900cdc3c6f023037aaf2d4407c4e8aaa909013a69539fb4688e2bd099db85', - md5: '39750d33d277617b322adbb917f7b626', - }, - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - Description: 'UsoClient', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - OriginalFileName: 'UsoClient', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.17763.1007 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222846, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 08:44:22.455\nProcessGuid: {ce1d3c9b-f2e6-5f55-bc15-000000000b00}\nProcessId: 3864\nImage: C:\\Windows\\System32\\UsoClient.exe\nFileVersion: 10.0.17763.1007 (WinBuild.160101.0800)\nDescription: UsoClient\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: UsoClient\nCommandLine: C:\\Windows\\system32\\usoclient.exe StartScan\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=EBF56AD89D4740359D5D3D5370B31E56614BBB79,MD5=39750D33D277617B322ADBB917F7B626,SHA256=DF3900CDC3C6F023037AAF2D4407C4E8AAA909013A69539FB4688E2BD099DB85,IMPHASH=2510E8A4554AEF2CAF0A913BE015929F\nParentProcessGuid: {ce1d3c9b-b9b1-5f34-1c00-000000000b00}\nParentProcessId: 1060\nParentImage: C:\\Windows\\System32\\svchost.exe\nParentCommandLine: C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], + 'process.name': ['UsoClient.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T08:44:22.455Z', - related: { - user: 'SYSTEM', - hash: [ - 'ebf56ad89d4740359d5d3d5370b31e56614bbb79', - '39750d33d277617b322adbb917f7b626', - 'df3900cdc3c6f023037aaf2d4407c4e8aaa909013a69539fb4688e2bd099db85', - '2510e8a4554aef2caf0a913be015929f', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T08:44:24.029Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: 'ebf56ad89d4740359d5d3d5370b31e56614bbb79', - imphash: '2510e8a4554aef2caf0a913be015929f', - sha256: - 'df3900cdc3c6f023037aaf2d4407c4e8aaa909013a69539fb4688e2bd099db85', - md5: '39750d33d277617b322adbb917f7b626', - }, }, }, ], @@ -1721,15 +555,11 @@ export const mockSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.process-default-000001', _id: 'Ziw-Z3QBB-gskcly0vqU', _score: null, - _source: { - process: { - args: ['/etc/cron.daily/apt-compat'], - name: 'apt-compat', - }, - user: { - name: 'root', - id: 0, - }, + fields: { + 'process.args': ['/etc/cron.daily/apt-compat'], + 'process.name': ['apt-compat'], + 'user.name': ['root'], + 'user.id': [0], }, sort: [1599459901154], }, @@ -1752,113 +582,13 @@ export const mockSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.process-default-000001', _id: 'Ziw-Z3QBB-gskcly0vqU', _score: 0, - _source: { - agent: { - id: 'b1e3298e-10be-4032-b1ee-5a4cbb280aa1', - type: 'endpoint', - version: '7.9.1', - }, - process: { - Ext: { - ancestry: [ - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYyLTEzMjQzOTMzNTAxLjUzOTIzMzAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYxLTEzMjQzOTMzNTAxLjUzMjIzMTAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYxLTEzMjQzOTMzNTAxLjUyODg0MzAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYwLTEzMjQzOTMzNTAxLjUyMDI5ODAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYwLTEzMjQzOTMzNTAxLjUwNzM4MjAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODU5LTEzMjQzOTMzNTAxLjc3NTM1MDAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTUyNC0xMzIzNjA4NTMzMC4w', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEtMTMyMzYwODUzMjIuMA==', - ], - }, - args: ['/etc/cron.daily/apt-compat'], - parent: { - name: 'run-parts', - pid: 13861, - entity_id: - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYyLTEzMjQzOTMzNTAxLjUzOTIzMzAw', - executable: '/bin/run-parts', - }, - name: 'apt-compat', - pid: 13862, - args_count: 1, - entity_id: - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYyLTEzMjQzOTMzNTAxLjU0NDY0MDAw', - command_line: '/etc/cron.daily/apt-compat', - executable: '/etc/cron.daily/apt-compat', - hash: { - sha1: '61445721d0b5d86ac0a8386a4ceef450118f4fbb', - sha256: - '8eeae3a9df22621d51062e4dadfc5c63b49732b38a37b5d4e52c99c2237e5767', - md5: 'bc4a71cbcaeed4179f25d798257fa980', - }, - }, - message: 'Endpoint process event', + fields: { + 'process.args': ['/etc/cron.daily/apt-compat'], + 'process.name': ['apt-compat'], + 'host.name': ['siem-kibana'], + 'user.name': ['root'], + 'user.id': [0], '@timestamp': '2020-09-07T06:25:01.154464000Z', - ecs: { - version: '1.5.0', - }, - data_stream: { - namespace: 'default', - type: 'logs', - dataset: 'endpoint.events.process', - }, - elastic: { - agent: { - id: 'ebee9a13-9ae3-4a55-9cb7-72ddf053055f', - }, - }, - host: { - hostname: 'siem-kibana', - os: { - Ext: { - variant: 'Debian', - }, - kernel: '4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27)', - name: 'Linux', - family: 'debian', - version: '9', - platform: 'debian', - full: 'Debian 9', - }, - ip: ['127.0.0.1', '::1', '10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'e50acb49-820b-c60a-392d-2ef75f276301', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - sequence: 197060, - ingested: '2020-09-07T06:26:44.476888Z', - created: '2020-09-07T06:25:01.154464000Z', - kind: 'event', - module: 'endpoint', - action: 'exec', - id: 'Lp6oofT0fzv0Auzq+++/kwCO', - category: ['process'], - type: ['start'], - dataset: 'endpoint.events.process', - }, - user: { - Ext: { - real: { - name: 'root', - id: 0, - }, - }, - name: 'root', - id: 0, - }, - group: { - Ext: { - real: { - name: 'root', - id: 0, - }, - }, - name: 'root', - id: 0, - }, }, }, ], @@ -1883,15 +613,11 @@ export const mockSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.process-default-000001', _id: 'aSw-Z3QBB-gskcly0vqU', _score: null, - _source: { - process: { - args: ['/etc/cron.daily/bsdmainutils'], - name: 'bsdmainutils', - }, - user: { - name: 'root', - id: 0, - }, + fields: { + 'process.args': ['/etc/cron.daily/bsdmainutils'], + 'process.name': ['bsdmainutils'], + 'user.name': ['root'], + 'user.id': [0], }, sort: [1599459901155], }, @@ -1914,113 +640,13 @@ export const mockSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.process-default-000001', _id: 'aSw-Z3QBB-gskcly0vqU', _score: 0, - _source: { - agent: { - id: 'b1e3298e-10be-4032-b1ee-5a4cbb280aa1', - type: 'endpoint', - version: '7.9.1', - }, - process: { - Ext: { - ancestry: [ - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYzLTEzMjQzOTMzNTAxLjU1MzMwMzAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYxLTEzMjQzOTMzNTAxLjUzMjIzMTAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYxLTEzMjQzOTMzNTAxLjUyODg0MzAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYwLTEzMjQzOTMzNTAxLjUyMDI5ODAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYwLTEzMjQzOTMzNTAxLjUwNzM4MjAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODU5LTEzMjQzOTMzNTAxLjc3NTM1MDAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTUyNC0xMzIzNjA4NTMzMC4w', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEtMTMyMzYwODUzMjIuMA==', - ], - }, - args: ['/etc/cron.daily/bsdmainutils'], - parent: { - name: 'run-parts', - pid: 13861, - entity_id: - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYzLTEzMjQzOTMzNTAxLjU1MzMwMzAw', - executable: '/bin/run-parts', - }, - name: 'bsdmainutils', - pid: 13863, - args_count: 1, - entity_id: - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYzLTEzMjQzOTMzNTAxLjU1ODEyMDAw', - command_line: '/etc/cron.daily/bsdmainutils', - executable: '/etc/cron.daily/bsdmainutils', - hash: { - sha1: 'fd24f1f3986e5527e804c4dccddee29ff42cb682', - sha256: - 'a68002bf1dc9f42a150087b00437448a46f7cae6755ecddca70a6d3c9d20a14b', - md5: '559387f792462a62e3efb1d573e38d11', - }, - }, - message: 'Endpoint process event', + fields: { + 'process.args': ['/etc/cron.daily/bsdmainutils'], + 'process.name': ['bsdmainutils'], + 'host.name': ['siem-kibana'], + 'user.name': ['root'], + 'user.id': [0], '@timestamp': '2020-09-07T06:25:01.155812000Z', - ecs: { - version: '1.5.0', - }, - data_stream: { - namespace: 'default', - type: 'logs', - dataset: 'endpoint.events.process', - }, - elastic: { - agent: { - id: 'ebee9a13-9ae3-4a55-9cb7-72ddf053055f', - }, - }, - host: { - hostname: 'siem-kibana', - os: { - Ext: { - variant: 'Debian', - }, - kernel: '4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27)', - name: 'Linux', - family: 'debian', - version: '9', - platform: 'debian', - full: 'Debian 9', - }, - ip: ['127.0.0.1', '::1', '10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'e50acb49-820b-c60a-392d-2ef75f276301', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - sequence: 197063, - ingested: '2020-09-07T06:26:44.477164Z', - created: '2020-09-07T06:25:01.155812000Z', - kind: 'event', - module: 'endpoint', - action: 'exec', - id: 'Lp6oofT0fzv0Auzq+++/kwCZ', - category: ['process'], - type: ['start'], - dataset: 'endpoint.events.process', - }, - user: { - Ext: { - real: { - name: 'root', - id: 0, - }, - }, - name: 'root', - id: 0, - }, - group: { - Ext: { - real: { - name: 'root', - id: 0, - }, - }, - name: 'root', - id: 0, - }, }, }, ], @@ -2077,18 +703,14 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'ayrMZnQBB-gskcly0w7l', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', - 'WD', - '/q', - ], - name: 'AM_Delta_Patch_1.323.631.0.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', + 'WD', + '/q', + ], + 'process.name': ['AM_Delta_Patch_1.323.631.0.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599452531834], }, @@ -2111,161 +733,16 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'ayrMZnQBB-gskcly0w7l', _score: 0, - _source: { - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - process: { - args: [ - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', - 'WD', - '/q', - ], - parent: { - args: [ - 'C:\\Windows\\system32\\wuauclt.exe', - '/RunHandlerComServer', - ], - name: 'wuauclt.exe', - pid: 4844, - entity_id: '{ce1d3c9b-b573-5f55-b115-000000000b00}', - executable: 'C:\\Windows\\System32\\wuauclt.exe', - command_line: - '"C:\\Windows\\system32\\wuauclt.exe" /RunHandlerComServer', - }, - pe: { - imphash: 'f96ec1e772808eb81774fb67a4ac229e', - }, - name: 'AM_Delta_Patch_1.323.631.0.exe', - pid: 4608, - working_directory: - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\', - entity_id: '{ce1d3c9b-b573-5f55-b215-000000000b00}', - executable: - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', - command_line: - '"C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe" WD /q', - hash: { - sha1: '94eb7f83ddee6942ec5bdb8e218b5bc942158cb3', - sha256: - '562c58193ba7878b396ebc3fb2dccece7ea0d5c6c7d52fc3ac10b62b894260eb', - md5: '5608b911376da958ed93a7f9428ad0b9', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'Microsoft Antimalware WU Stub', - OriginalFileName: 'AM_Delta_Patch_1.323.631.0.exe', - IntegrityLevel: 'System', - TerminalSessionId: '0', - FileVersion: '1.323.673.0', - Product: 'Microsoft Malware Protection', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222529, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 04:22:11.834\nProcessGuid: {ce1d3c9b-b573-5f55-b215-000000000b00}\nProcessId: 4608\nImage: C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe\nFileVersion: 1.323.673.0\nDescription: Microsoft Antimalware WU Stub\nProduct: Microsoft Malware Protection\nCompany: Microsoft Corporation\nOriginalFileName: AM_Delta_Patch_1.323.631.0.exe\nCommandLine: "C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe" WD /q\nCurrentDirectory: C:\\Windows\\SoftwareDistribution\\Download\\Install\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=94EB7F83DDEE6942EC5BDB8E218B5BC942158CB3,MD5=5608B911376DA958ED93A7F9428AD0B9,SHA256=562C58193BA7878B396EBC3FB2DCCECE7EA0D5C6C7D52FC3AC10B62B894260EB,IMPHASH=F96EC1E772808EB81774FB67A4AC229E\nParentProcessGuid: {ce1d3c9b-b573-5f55-b115-000000000b00}\nParentProcessId: 4844\nParentImage: C:\\Windows\\System32\\wuauclt.exe\nParentCommandLine: "C:\\Windows\\system32\\wuauclt.exe" /RunHandlerComServer', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.631.0.exe', + 'WD', + '/q', + ], + 'process.name': ['AM_Delta_Patch_1.323.631.0.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T04:22:11.834Z', - ecs: { - version: '1.5.0', - }, - related: { - user: 'SYSTEM', - hash: [ - '94eb7f83ddee6942ec5bdb8e218b5bc942158cb3', - '5608b911376da958ed93a7f9428ad0b9', - '562c58193ba7878b396ebc3fb2dccece7ea0d5c6c7d52fc3ac10b62b894260eb', - 'f96ec1e772808eb81774fb67a4ac229e', - ], - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T04:22:12.727Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - type: ['start', 'process_start'], - category: ['process'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: '94eb7f83ddee6942ec5bdb8e218b5bc942158cb3', - imphash: 'f96ec1e772808eb81774fb67a4ac229e', - sha256: - '562c58193ba7878b396ebc3fb2dccece7ea0d5c6c7d52fc3ac10b62b894260eb', - md5: '5608b911376da958ed93a7f9428ad0b9', - }, }, }, ], @@ -2290,18 +767,14 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'M-GvaHQBA6bGZw2uBoYz', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', - 'WD', - '/q', - ], - name: 'AM_Delta_Patch_1.323.673.0.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', + 'WD', + '/q', + ], + 'process.name': ['AM_Delta_Patch_1.323.673.0.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599484132366], }, @@ -2324,161 +797,16 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'M-GvaHQBA6bGZw2uBoYz', _score: 0, - _source: { - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - name: 'siem-windows', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - process: { - args: [ - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', - 'WD', - '/q', - ], - parent: { - args: [ - 'C:\\Windows\\system32\\wuauclt.exe', - '/RunHandlerComServer', - ], - name: 'wuauclt.exe', - pid: 4548, - entity_id: '{ce1d3c9b-30e3-5f56-ca15-000000000b00}', - executable: 'C:\\Windows\\System32\\wuauclt.exe', - command_line: - '"C:\\Windows\\system32\\wuauclt.exe" /RunHandlerComServer', - }, - pe: { - imphash: 'f96ec1e772808eb81774fb67a4ac229e', - }, - name: 'AM_Delta_Patch_1.323.673.0.exe', - working_directory: - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\', - pid: 4684, - entity_id: '{ce1d3c9b-30e4-5f56-cb15-000000000b00}', - executable: - 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', - command_line: - '"C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe" WD /q', - hash: { - sha1: 'ae1e653f1e53dcd34415a35335f9e44d2a33be65', - sha256: - '4382c96613850568d003c02ba0a285f6d2ef9b8c20790ffa2b35641bc831293f', - md5: 'd088fcf98bb9aa1e8f07a36b05011555', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'Microsoft Antimalware WU Stub', - OriginalFileName: 'AM_Delta_Patch_1.323.673.0.exe', - IntegrityLevel: 'System', - TerminalSessionId: '0', - FileVersion: '1.323.693.0', - Product: 'Microsoft Malware Protection', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 223146, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 13:08:52.366\nProcessGuid: {ce1d3c9b-30e4-5f56-cb15-000000000b00}\nProcessId: 4684\nImage: C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe\nFileVersion: 1.323.693.0\nDescription: Microsoft Antimalware WU Stub\nProduct: Microsoft Malware Protection\nCompany: Microsoft Corporation\nOriginalFileName: AM_Delta_Patch_1.323.673.0.exe\nCommandLine: "C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe" WD /q\nCurrentDirectory: C:\\Windows\\SoftwareDistribution\\Download\\Install\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=AE1E653F1E53DCD34415A35335F9E44D2A33BE65,MD5=D088FCF98BB9AA1E8F07A36B05011555,SHA256=4382C96613850568D003C02BA0A285F6D2EF9B8C20790FFA2B35641BC831293F,IMPHASH=F96EC1E772808EB81774FB67A4AC229E\nParentProcessGuid: {ce1d3c9b-30e3-5f56-ca15-000000000b00}\nParentProcessId: 4548\nParentImage: C:\\Windows\\System32\\wuauclt.exe\nParentCommandLine: "C:\\Windows\\system32\\wuauclt.exe" /RunHandlerComServer', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.323.673.0.exe', + 'WD', + '/q', + ], + 'process.name': ['AM_Delta_Patch_1.323.673.0.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T13:08:52.366Z', - ecs: { - version: '1.5.0', - }, - related: { - user: 'SYSTEM', - hash: [ - 'ae1e653f1e53dcd34415a35335f9e44d2a33be65', - 'd088fcf98bb9aa1e8f07a36b05011555', - '4382c96613850568d003c02ba0a285f6d2ef9b8c20790ffa2b35641bc831293f', - 'f96ec1e772808eb81774fb67a4ac229e', - ], - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T13:08:53.889Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: 'ae1e653f1e53dcd34415a35335f9e44d2a33be65', - imphash: 'f96ec1e772808eb81774fb67a4ac229e', - sha256: - '4382c96613850568d003c02ba0a285f6d2ef9b8c20790ffa2b35641bc831293f', - md5: 'd088fcf98bb9aa1e8f07a36b05011555', - }, }, }, ], @@ -2503,14 +831,10 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'cinEZnQBB-gskclyvNmU', _score: null, - _source: { - process: { - args: ['C:\\Windows\\system32\\devicecensus.exe'], - name: 'DeviceCensus.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\devicecensus.exe'], + 'process.name': ['DeviceCensus.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599452000791], }, @@ -2533,150 +857,12 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'cinEZnQBB-gskclyvNmU', _score: 0, - _source: { - process: { - args: ['C:\\Windows\\system32\\devicecensus.exe'], - parent: { - args: ['C:\\Windows\\system32\\svchost.exe', '-k', 'netsvcs', '-p'], - name: 'svchost.exe', - pid: 1060, - entity_id: '{ce1d3c9b-b9b1-5f34-1c00-000000000b00}', - executable: 'C:\\Windows\\System32\\svchost.exe', - command_line: 'C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - }, - pe: { - imphash: '0cdb6b589f0a125609d8df646de0ea86', - }, - name: 'DeviceCensus.exe', - pid: 5016, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-b360-5f55-a115-000000000b00}', - executable: 'C:\\Windows\\System32\\DeviceCensus.exe', - command_line: 'C:\\Windows\\system32\\devicecensus.exe', - hash: { - sha1: '9e488437b2233e5ad9abd3151ec28ea51eb64c2d', - sha256: - 'dbea7473d5e7b3b4948081dacc6e35327d5a588f4fd0a2d68184bffd10439296', - md5: '8159944c79034d2bcabf73d461a7e643', - }, - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - name: 'siem-windows', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - Description: 'Device Census', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - OriginalFileName: 'DeviceCensus.exe', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.18362.1035 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222507, - task: 'Process Create (rule: ProcessCreate)', - event_id: 1, - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 04:13:20.791\nProcessGuid: {ce1d3c9b-b360-5f55-a115-000000000b00}\nProcessId: 5016\nImage: C:\\Windows\\System32\\DeviceCensus.exe\nFileVersion: 10.0.18362.1035 (WinBuild.160101.0800)\nDescription: Device Census\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: DeviceCensus.exe\nCommandLine: C:\\Windows\\system32\\devicecensus.exe\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=9E488437B2233E5AD9ABD3151EC28EA51EB64C2D,MD5=8159944C79034D2BCABF73D461A7E643,SHA256=DBEA7473D5E7B3B4948081DACC6E35327D5A588F4FD0A2D68184BFFD10439296,IMPHASH=0CDB6B589F0A125609D8DF646DE0EA86\nParentProcessGuid: {ce1d3c9b-b9b1-5f34-1c00-000000000b00}\nParentProcessId: 1060\nParentImage: C:\\Windows\\System32\\svchost.exe\nParentCommandLine: C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\devicecensus.exe'], + 'process.name': ['DeviceCensus.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T04:13:20.791Z', - related: { - user: 'SYSTEM', - hash: [ - '9e488437b2233e5ad9abd3151ec28ea51eb64c2d', - '8159944c79034d2bcabf73d461a7e643', - 'dbea7473d5e7b3b4948081dacc6e35327d5a588f4fd0a2d68184bffd10439296', - '0cdb6b589f0a125609d8df646de0ea86', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T04:13:22.458Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: '9e488437b2233e5ad9abd3151ec28ea51eb64c2d', - imphash: '0cdb6b589f0a125609d8df646de0ea86', - sha256: - 'dbea7473d5e7b3b4948081dacc6e35327d5a588f4fd0a2d68184bffd10439296', - md5: '8159944c79034d2bcabf73d461a7e643', - }, }, }, ], @@ -2701,14 +887,10 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'HNKSZHQBA6bGZw2uCtRk', _score: null, - _source: { - process: { - args: ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], - name: 'DiskSnapshot.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], + 'process.name': ['DiskSnapshot.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599415124040], }, @@ -2731,150 +913,12 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'HNKSZHQBA6bGZw2uCtRk', _score: 0, - _source: { - process: { - args: ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], - parent: { - args: ['C:\\Windows\\system32\\svchost.exe', '-k', 'netsvcs', '-p'], - name: 'svchost.exe', - pid: 1060, - entity_id: '{ce1d3c9b-b9b1-5f34-1c00-000000000b00}', - executable: 'C:\\Windows\\System32\\svchost.exe', - command_line: 'C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - }, - pe: { - imphash: '69bdabb73b409f40ad05f057cec29380', - }, - name: 'DiskSnapshot.exe', - pid: 3120, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-2354-5f55-6415-000000000b00}', - command_line: 'C:\\Windows\\system32\\disksnapshot.exe -z', - executable: 'C:\\Windows\\System32\\DiskSnapshot.exe', - hash: { - sha1: '61b4d8d4757e15259e1e92c8236f37237b5380d1', - sha256: - 'c7b9591eb4dd78286615401c138c7c1a89f0e358caae1786de2c3b08e904ffdc', - md5: 'ece311ff51bd847a3874bfac85449c6b', - }, - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'DiskSnapshot.exe', - OriginalFileName: 'DiskSnapshot.exe', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.17763.652 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 221799, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-06 17:58:44.040\nProcessGuid: {ce1d3c9b-2354-5f55-6415-000000000b00}\nProcessId: 3120\nImage: C:\\Windows\\System32\\DiskSnapshot.exe\nFileVersion: 10.0.17763.652 (WinBuild.160101.0800)\nDescription: DiskSnapshot.exe\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: DiskSnapshot.exe\nCommandLine: C:\\Windows\\system32\\disksnapshot.exe -z\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=61B4D8D4757E15259E1E92C8236F37237B5380D1,MD5=ECE311FF51BD847A3874BFAC85449C6B,SHA256=C7B9591EB4DD78286615401C138C7C1A89F0E358CAAE1786DE2C3B08E904FFDC,IMPHASH=69BDABB73B409F40AD05F057CEC29380\nParentProcessGuid: {ce1d3c9b-b9b1-5f34-1c00-000000000b00}\nParentProcessId: 1060\nParentImage: C:\\Windows\\System32\\svchost.exe\nParentCommandLine: C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\disksnapshot.exe', '-z'], + 'process.name': ['DiskSnapshot.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-06T17:58:44.040Z', - related: { - user: 'SYSTEM', - hash: [ - '61b4d8d4757e15259e1e92c8236f37237b5380d1', - 'ece311ff51bd847a3874bfac85449c6b', - 'c7b9591eb4dd78286615401c138c7c1a89f0e358caae1786de2c3b08e904ffdc', - '69bdabb73b409f40ad05f057cec29380', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-06T17:58:45.606Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: '61b4d8d4757e15259e1e92c8236f37237b5380d1', - imphash: '69bdabb73b409f40ad05f057cec29380', - sha256: - 'c7b9591eb4dd78286615401c138c7c1a89f0e358caae1786de2c3b08e904ffdc', - md5: 'ece311ff51bd847a3874bfac85449c6b', - }, }, }, ], @@ -2899,17 +943,13 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '2zncaHQBB-gskcly1QaD', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', - '{6BB79B50-2038-4A10-B513-2FAC72FF213E}', - ], - name: 'DismHost.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', + '{6BB79B50-2038-4A10-B513-2FAC72FF213E}', + ], + 'process.name': ['DismHost.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599487135371], }, @@ -2932,159 +972,15 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '2zncaHQBB-gskcly1QaD', _score: 0, - _source: { - process: { - args: [ - 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', - '{6BB79B50-2038-4A10-B513-2FAC72FF213E}', - ], - parent: { - args: [ - 'C:\\ProgramData\\Microsoft\\Windows Defender\\platform\\4.18.2008.9-0\\MsMpEng.exe', - ], - name: 'MsMpEng.exe', - pid: 184, - entity_id: '{ce1d3c9b-1b55-5f4f-4913-000000000b00}', - executable: - 'C:\\ProgramData\\Microsoft\\Windows Defender\\Platform\\4.18.2008.9-0\\MsMpEng.exe', - command_line: - '"C:\\ProgramData\\Microsoft\\Windows Defender\\platform\\4.18.2008.9-0\\MsMpEng.exe"', - }, - pe: { - imphash: 'a644b5814b05375757429dfb05524479', - }, - name: 'DismHost.exe', - pid: 1500, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-3c9f-5f56-d315-000000000b00}', - executable: - 'C:\\Windows\\Temp\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\DismHost.exe', - command_line: - 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe {6BB79B50-2038-4A10-B513-2FAC72FF213E}', - hash: { - sha1: 'a8a65b6a45a988f06e17ebd04e5462ca730d2337', - sha256: - 'b94317b7c665f1cec965e3322e0aa26c8be29eaf5830fb7fcd7e14ae88a8cf22', - md5: '5867dc628a444f2393f7eff007bd4417', - }, - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - name: 'siem-windows', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - type: 'winlogbeat', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'Dism Host Servicing Process', - OriginalFileName: 'DismHost.exe', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.17763.771 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 223274, - task: 'Process Create (rule: ProcessCreate)', - event_id: 1, - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 13:58:55.371\nProcessGuid: {ce1d3c9b-3c9f-5f56-d315-000000000b00}\nProcessId: 1500\nImage: C:\\Windows\\Temp\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\DismHost.exe\nFileVersion: 10.0.17763.771 (WinBuild.160101.0800)\nDescription: Dism Host Servicing Process\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: DismHost.exe\nCommandLine: C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe {6BB79B50-2038-4A10-B513-2FAC72FF213E}\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=A8A65B6A45A988F06E17EBD04E5462CA730D2337,MD5=5867DC628A444F2393F7EFF007BD4417,SHA256=B94317B7C665F1CEC965E3322E0AA26C8BE29EAF5830FB7FCD7E14AE88A8CF22,IMPHASH=A644B5814B05375757429DFB05524479\nParentProcessGuid: {ce1d3c9b-1b55-5f4f-4913-000000000b00}\nParentProcessId: 184\nParentImage: C:\\ProgramData\\Microsoft\\Windows Defender\\Platform\\4.18.2008.9-0\\MsMpEng.exe\nParentCommandLine: "C:\\ProgramData\\Microsoft\\Windows Defender\\platform\\4.18.2008.9-0\\MsMpEng.exe"', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\TEMP\\88C4F57A-8744-4EA6-824E-88FEF8A0E9DD\\dismhost.exe', + '{6BB79B50-2038-4A10-B513-2FAC72FF213E}', + ], + 'process.name': ['DismHost.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T13:58:55.371Z', - related: { - user: 'SYSTEM', - hash: [ - 'a8a65b6a45a988f06e17ebd04e5462ca730d2337', - '5867dc628a444f2393f7eff007bd4417', - 'b94317b7c665f1cec965e3322e0aa26c8be29eaf5830fb7fcd7e14ae88a8cf22', - 'a644b5814b05375757429dfb05524479', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T13:58:56.138Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: 'a8a65b6a45a988f06e17ebd04e5462ca730d2337', - imphash: 'a644b5814b05375757429dfb05524479', - sha256: - 'b94317b7c665f1cec965e3322e0aa26c8be29eaf5830fb7fcd7e14ae88a8cf22', - md5: '5867dc628a444f2393f7eff007bd4417', - }, }, }, ], @@ -3109,18 +1005,14 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'gdVuZXQBA6bGZw2uFsPP', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\System32\\sihclient.exe', - '/cv', - '33nfV21X50ie84HvATAt1w.0.1', - ], - name: 'SIHClient.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\System32\\sihclient.exe', + '/cv', + '33nfV21X50ie84HvATAt1w.0.1', + ], + 'process.name': ['SIHClient.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599429545370], }, @@ -3143,162 +1035,16 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'gdVuZXQBA6bGZw2uFsPP', _score: 0, - _source: { - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - process: { - args: [ - 'C:\\Windows\\System32\\sihclient.exe', - '/cv', - '33nfV21X50ie84HvATAt1w.0.1', - ], - parent: { - args: [ - 'C:\\Windows\\System32\\Upfc.exe', - '/launchtype', - 'periodic', - '/cv', - '33nfV21X50ie84HvATAt1w.0', - ], - name: 'upfc.exe', - pid: 4328, - entity_id: '{ce1d3c9b-5b8b-5f55-7815-000000000b00}', - executable: 'C:\\Windows\\System32\\upfc.exe', - command_line: - 'C:\\Windows\\System32\\Upfc.exe /launchtype periodic /cv 33nfV21X50ie84HvATAt1w.0', - }, - pe: { - imphash: '3bbd1eea2778ee3dcd883a4d5533aec3', - }, - name: 'SIHClient.exe', - pid: 2780, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-5ba9-5f55-8815-000000000b00}', - executable: 'C:\\Windows\\System32\\SIHClient.exe', - command_line: - 'C:\\Windows\\System32\\sihclient.exe /cv 33nfV21X50ie84HvATAt1w.0.1', - hash: { - sha1: '145ef8d82cf1e451381584cd9565a2d35a442504', - sha256: - '0e0bb70ae1888060b3ffb9a320963551b56dd0d4ce0b5dc1c8fadda4b7bf3f6a', - md5: 'dc1e380b36f4a8309f363d3809e607b8', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - Description: 'SIH Client', - OriginalFileName: 'sihclient.exe', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.17763.1217 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222106, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-06 21:59:05.370\nProcessGuid: {ce1d3c9b-5ba9-5f55-8815-000000000b00}\nProcessId: 2780\nImage: C:\\Windows\\System32\\SIHClient.exe\nFileVersion: 10.0.17763.1217 (WinBuild.160101.0800)\nDescription: SIH Client\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: sihclient.exe\nCommandLine: C:\\Windows\\System32\\sihclient.exe /cv 33nfV21X50ie84HvATAt1w.0.1\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=145EF8D82CF1E451381584CD9565A2D35A442504,MD5=DC1E380B36F4A8309F363D3809E607B8,SHA256=0E0BB70AE1888060B3FFB9A320963551B56DD0D4CE0B5DC1C8FADDA4B7BF3F6A,IMPHASH=3BBD1EEA2778EE3DCD883A4D5533AEC3\nParentProcessGuid: {ce1d3c9b-5b8b-5f55-7815-000000000b00}\nParentProcessId: 4328\nParentImage: C:\\Windows\\System32\\upfc.exe\nParentCommandLine: C:\\Windows\\System32\\Upfc.exe /launchtype periodic /cv 33nfV21X50ie84HvATAt1w.0', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\System32\\sihclient.exe', + '/cv', + '33nfV21X50ie84HvATAt1w.0.1', + ], + 'process.name': ['SIHClient.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-06T21:59:05.370Z', - related: { - user: 'SYSTEM', - hash: [ - '145ef8d82cf1e451381584cd9565a2d35a442504', - 'dc1e380b36f4a8309f363d3809e607b8', - '0e0bb70ae1888060b3ffb9a320963551b56dd0d4ce0b5dc1c8fadda4b7bf3f6a', - '3bbd1eea2778ee3dcd883a4d5533aec3', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - kind: 'event', - created: '2020-09-06T21:59:06.713Z', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: '145ef8d82cf1e451381584cd9565a2d35a442504', - imphash: '3bbd1eea2778ee3dcd883a4d5533aec3', - sha256: - '0e0bb70ae1888060b3ffb9a320963551b56dd0d4ce0b5dc1c8fadda4b7bf3f6a', - md5: 'dc1e380b36f4a8309f363d3809e607b8', - }, }, }, ], @@ -3323,16 +1069,12 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '6NmKZnQBA6bGZw2uma12', _score: null, - _source: { - process: { - args: [ - 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', - ], - name: 'SpeechModelDownload.exe', - }, - user: { - name: 'NETWORK SERVICE', - }, + fields: { + 'process.args': [ + 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', + ], + 'process.name': ['SpeechModelDownload.exe'], + 'user.name': ['NETWORK SERVICE'], }, sort: [1599448191225], }, @@ -3355,154 +1097,14 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: '6NmKZnQBA6bGZw2uma12', _score: 0, - _source: { - process: { - args: [ - 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', - ], - parent: { - args: ['C:\\Windows\\system32\\svchost.exe', '-k', 'netsvcs', '-p'], - name: 'svchost.exe', - pid: 1060, - entity_id: '{ce1d3c9b-b9b1-5f34-1c00-000000000b00}', - executable: 'C:\\Windows\\System32\\svchost.exe', - command_line: 'C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - }, - pe: { - imphash: '23bd5f904494d14029d9263cebae088d', - }, - name: 'SpeechModelDownload.exe', - working_directory: 'C:\\Windows\\system32\\', - pid: 4328, - entity_id: '{ce1d3c9b-a47f-5f55-9915-000000000b00}', - hash: { - sha1: '03e6e81192621dfd873814de3787c6e7d6af1509', - sha256: - '963fd9dc1b82c44d00eb91d61e2cb442af7357e3a603c23d469df53a6376f073', - md5: '3fd687e97e03d303e02bb37ec85de962', - }, - executable: - 'C:\\Windows\\System32\\Speech_OneCore\\common\\SpeechModelDownload.exe', - command_line: - 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - LogonGuid: '{ce1d3c9b-b9ac-5f34-e403-000000000000}', - Description: 'Speech Model Download Executable', - OriginalFileName: 'SpeechModelDownload.exe', - IntegrityLevel: 'System', - TerminalSessionId: '0', - FileVersion: '10.0.17763.1369 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e4', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222431, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 03:09:51.225\nProcessGuid: {ce1d3c9b-a47f-5f55-9915-000000000b00}\nProcessId: 4328\nImage: C:\\Windows\\System32\\Speech_OneCore\\common\\SpeechModelDownload.exe\nFileVersion: 10.0.17763.1369 (WinBuild.160101.0800)\nDescription: Speech Model Download Executable\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: SpeechModelDownload.exe\nCommandLine: C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\NETWORK SERVICE\nLogonGuid: {ce1d3c9b-b9ac-5f34-e403-000000000000}\nLogonId: 0x3E4\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=03E6E81192621DFD873814DE3787C6E7D6AF1509,MD5=3FD687E97E03D303E02BB37EC85DE962,SHA256=963FD9DC1B82C44D00EB91D61E2CB442AF7357E3A603C23D469DF53A6376F073,IMPHASH=23BD5F904494D14029D9263CEBAE088D\nParentProcessGuid: {ce1d3c9b-b9b1-5f34-1c00-000000000b00}\nParentProcessId: 1060\nParentImage: C:\\Windows\\System32\\svchost.exe\nParentCommandLine: C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': [ + 'C:\\Windows\\system32\\speech_onecore\\common\\SpeechModelDownload.exe', + ], + 'process.name': ['SpeechModelDownload.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['NETWORK SERVICE'], '@timestamp': '2020-09-07T03:09:51.225Z', - related: { - user: 'NETWORK SERVICE', - hash: [ - '03e6e81192621dfd873814de3787c6e7d6af1509', - '3fd687e97e03d303e02bb37ec85de962', - '963fd9dc1b82c44d00eb91d61e2cb442af7357e3a603c23d469df53a6376f073', - '23bd5f904494d14029d9263cebae088d', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - kind: 'event', - created: '2020-09-07T03:09:52.370Z', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - type: ['start', 'process_start'], - category: ['process'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'NETWORK SERVICE', - }, - hash: { - sha1: '03e6e81192621dfd873814de3787c6e7d6af1509', - imphash: '23bd5f904494d14029d9263cebae088d', - sha256: - '963fd9dc1b82c44d00eb91d61e2cb442af7357e3a603c23d469df53a6376f073', - md5: '3fd687e97e03d303e02bb37ec85de962', - }, }, }, ], @@ -3527,14 +1129,10 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'Pi68Z3QBc39KFIJb3txa', _score: null, - _source: { - process: { - args: ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], - name: 'UsoClient.exe', - }, - user: { - name: 'SYSTEM', - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], + 'process.name': ['UsoClient.exe'], + 'user.name': ['SYSTEM'], }, sort: [1599468262455], }, @@ -3557,150 +1155,12 @@ export const formattedSearchStrategyResponse = { _index: 'winlogbeat-8.0.0-2020.09.02-000001', _id: 'Pi68Z3QBc39KFIJb3txa', _score: 0, - _source: { - process: { - args: ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], - parent: { - args: ['C:\\Windows\\system32\\svchost.exe', '-k', 'netsvcs', '-p'], - name: 'svchost.exe', - pid: 1060, - entity_id: '{ce1d3c9b-b9b1-5f34-1c00-000000000b00}', - executable: 'C:\\Windows\\System32\\svchost.exe', - command_line: 'C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - }, - pe: { - imphash: '2510e8a4554aef2caf0a913be015929f', - }, - name: 'UsoClient.exe', - pid: 3864, - working_directory: 'C:\\Windows\\system32\\', - entity_id: '{ce1d3c9b-f2e6-5f55-bc15-000000000b00}', - command_line: 'C:\\Windows\\system32\\usoclient.exe StartScan', - executable: 'C:\\Windows\\System32\\UsoClient.exe', - hash: { - sha1: 'ebf56ad89d4740359d5d3d5370b31e56614bbb79', - sha256: - 'df3900cdc3c6f023037aaf2d4407c4e8aaa909013a69539fb4688e2bd099db85', - md5: '39750d33d277617b322adbb917f7b626', - }, - }, - agent: { - build_date: '2020-07-16 09:16:27 +0000 UTC ', - commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', - name: 'siem-windows', - id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', - ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', - type: 'winlogbeat', - version: '8.0.0', - user: { - name: 'inside_winlogbeat_user', - }, - }, - winlog: { - computer_name: 'siem-windows', - process: { - pid: 1252, - thread: { - id: 2896, - }, - }, - channel: 'Microsoft-Windows-Sysmon/Operational', - event_data: { - Company: 'Microsoft Corporation', - Description: 'UsoClient', - LogonGuid: '{ce1d3c9b-b9a7-5f34-e703-000000000000}', - OriginalFileName: 'UsoClient', - TerminalSessionId: '0', - IntegrityLevel: 'System', - FileVersion: '10.0.17763.1007 (WinBuild.160101.0800)', - Product: 'Microsoft® Windows® Operating System', - LogonId: '0x3e7', - RuleName: '-', - }, - opcode: 'Info', - version: 5, - record_id: 222846, - event_id: 1, - task: 'Process Create (rule: ProcessCreate)', - provider_guid: '{5770385f-c22a-43e0-bf4c-06f5698ffbd9}', - api: 'wineventlog', - provider_name: 'Microsoft-Windows-Sysmon', - user: { - identifier: 'S-1-5-18', - domain: 'NT AUTHORITY', - name: 'SYSTEM', - type: 'User', - }, - }, - log: { - level: 'information', - }, - message: - 'Process Create:\nRuleName: -\nUtcTime: 2020-09-07 08:44:22.455\nProcessGuid: {ce1d3c9b-f2e6-5f55-bc15-000000000b00}\nProcessId: 3864\nImage: C:\\Windows\\System32\\UsoClient.exe\nFileVersion: 10.0.17763.1007 (WinBuild.160101.0800)\nDescription: UsoClient\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: UsoClient\nCommandLine: C:\\Windows\\system32\\usoclient.exe StartScan\nCurrentDirectory: C:\\Windows\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {ce1d3c9b-b9a7-5f34-e703-000000000000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA1=EBF56AD89D4740359D5D3D5370B31E56614BBB79,MD5=39750D33D277617B322ADBB917F7B626,SHA256=DF3900CDC3C6F023037AAF2D4407C4E8AAA909013A69539FB4688E2BD099DB85,IMPHASH=2510E8A4554AEF2CAF0A913BE015929F\nParentProcessGuid: {ce1d3c9b-b9b1-5f34-1c00-000000000b00}\nParentProcessId: 1060\nParentImage: C:\\Windows\\System32\\svchost.exe\nParentCommandLine: C:\\Windows\\system32\\svchost.exe -k netsvcs -p', - cloud: { - availability_zone: 'us-central1-c', - instance: { - name: 'siem-windows', - id: '9156726559029788564', - }, - provider: 'gcp', - machine: { - type: 'g1-small', - }, - project: { - id: 'elastic-siem', - }, - }, + fields: { + 'process.args': ['C:\\Windows\\system32\\usoclient.exe', 'StartScan'], + 'process.name': ['UsoClient.exe'], + 'host.name': ['siem-windows'], + 'user.name': ['SYSTEM'], '@timestamp': '2020-09-07T08:44:22.455Z', - related: { - user: 'SYSTEM', - hash: [ - 'ebf56ad89d4740359d5d3d5370b31e56614bbb79', - '39750d33d277617b322adbb917f7b626', - 'df3900cdc3c6f023037aaf2d4407c4e8aaa909013a69539fb4688e2bd099db85', - '2510e8a4554aef2caf0a913be015929f', - ], - }, - ecs: { - version: '1.5.0', - }, - host: { - hostname: 'siem-windows', - os: { - build: '17763.1397', - kernel: '10.0.17763.1397 (WinBuild.160101.0800)', - name: 'Windows Server 2019 Datacenter', - family: 'windows', - version: '10.0', - platform: 'windows', - }, - ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], - name: 'siem-windows', - id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', - mac: ['42:01:0a:c8:00:0f'], - architecture: 'x86_64', - }, - event: { - code: 1, - provider: 'Microsoft-Windows-Sysmon', - created: '2020-09-07T08:44:24.029Z', - kind: 'event', - module: 'sysmon', - action: 'Process Create (rule: ProcessCreate)', - category: ['process'], - type: ['start', 'process_start'], - }, - user: { - domain: 'NT AUTHORITY', - name: 'SYSTEM', - }, - hash: { - sha1: 'ebf56ad89d4740359d5d3d5370b31e56614bbb79', - imphash: '2510e8a4554aef2caf0a913be015929f', - sha256: - 'df3900cdc3c6f023037aaf2d4407c4e8aaa909013a69539fb4688e2bd099db85', - md5: '39750d33d277617b322adbb917f7b626', - }, }, }, ], @@ -3725,15 +1185,11 @@ export const formattedSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.process-default-000001', _id: 'Ziw-Z3QBB-gskcly0vqU', _score: null, - _source: { - process: { - args: ['/etc/cron.daily/apt-compat'], - name: 'apt-compat', - }, - user: { - name: 'root', - id: 0, - }, + fields: { + 'process.args': ['/etc/cron.daily/apt-compat'], + 'process.name': ['apt-compat'], + 'user.name': ['root'], + 'user.id': [0], }, sort: [1599459901154], }, @@ -3756,113 +1212,13 @@ export const formattedSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.process-default-000001', _id: 'Ziw-Z3QBB-gskcly0vqU', _score: 0, - _source: { - agent: { - id: 'b1e3298e-10be-4032-b1ee-5a4cbb280aa1', - type: 'endpoint', - version: '7.9.1', - }, - process: { - Ext: { - ancestry: [ - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYyLTEzMjQzOTMzNTAxLjUzOTIzMzAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYxLTEzMjQzOTMzNTAxLjUzMjIzMTAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYxLTEzMjQzOTMzNTAxLjUyODg0MzAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYwLTEzMjQzOTMzNTAxLjUyMDI5ODAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYwLTEzMjQzOTMzNTAxLjUwNzM4MjAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODU5LTEzMjQzOTMzNTAxLjc3NTM1MDAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTUyNC0xMzIzNjA4NTMzMC4w', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEtMTMyMzYwODUzMjIuMA==', - ], - }, - args: ['/etc/cron.daily/apt-compat'], - parent: { - name: 'run-parts', - pid: 13861, - entity_id: - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYyLTEzMjQzOTMzNTAxLjUzOTIzMzAw', - executable: '/bin/run-parts', - }, - name: 'apt-compat', - pid: 13862, - args_count: 1, - entity_id: - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYyLTEzMjQzOTMzNTAxLjU0NDY0MDAw', - command_line: '/etc/cron.daily/apt-compat', - executable: '/etc/cron.daily/apt-compat', - hash: { - sha1: '61445721d0b5d86ac0a8386a4ceef450118f4fbb', - sha256: - '8eeae3a9df22621d51062e4dadfc5c63b49732b38a37b5d4e52c99c2237e5767', - md5: 'bc4a71cbcaeed4179f25d798257fa980', - }, - }, - message: 'Endpoint process event', + fields: { + 'process.args': ['/etc/cron.daily/apt-compat'], + 'process.name': ['apt-compat'], + 'host.name': ['siem-kibana'], + 'user.name': ['root'], + 'user.id': [0], '@timestamp': '2020-09-07T06:25:01.154464000Z', - ecs: { - version: '1.5.0', - }, - data_stream: { - namespace: 'default', - type: 'logs', - dataset: 'endpoint.events.process', - }, - elastic: { - agent: { - id: 'ebee9a13-9ae3-4a55-9cb7-72ddf053055f', - }, - }, - host: { - hostname: 'siem-kibana', - os: { - Ext: { - variant: 'Debian', - }, - kernel: '4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27)', - name: 'Linux', - family: 'debian', - version: '9', - platform: 'debian', - full: 'Debian 9', - }, - ip: ['127.0.0.1', '::1', '10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'e50acb49-820b-c60a-392d-2ef75f276301', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - sequence: 197060, - ingested: '2020-09-07T06:26:44.476888Z', - created: '2020-09-07T06:25:01.154464000Z', - kind: 'event', - module: 'endpoint', - action: 'exec', - id: 'Lp6oofT0fzv0Auzq+++/kwCO', - category: ['process'], - type: ['start'], - dataset: 'endpoint.events.process', - }, - user: { - Ext: { - real: { - name: 'root', - id: 0, - }, - }, - name: 'root', - id: 0, - }, - group: { - Ext: { - real: { - name: 'root', - id: 0, - }, - }, - name: 'root', - id: 0, - }, }, }, ], @@ -3887,15 +1243,11 @@ export const formattedSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.process-default-000001', _id: 'aSw-Z3QBB-gskcly0vqU', _score: null, - _source: { - process: { - args: ['/etc/cron.daily/bsdmainutils'], - name: 'bsdmainutils', - }, - user: { - name: 'root', - id: 0, - }, + fields: { + 'process.args': ['/etc/cron.daily/bsdmainutils'], + 'process.name': ['bsdmainutils'], + 'user.name': ['root'], + 'user.id': [0], }, sort: [1599459901155], }, @@ -3918,113 +1270,13 @@ export const formattedSearchStrategyResponse = { _index: '.ds-logs-endpoint.events.process-default-000001', _id: 'aSw-Z3QBB-gskcly0vqU', _score: 0, - _source: { - agent: { - id: 'b1e3298e-10be-4032-b1ee-5a4cbb280aa1', - type: 'endpoint', - version: '7.9.1', - }, - process: { - Ext: { - ancestry: [ - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYzLTEzMjQzOTMzNTAxLjU1MzMwMzAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYxLTEzMjQzOTMzNTAxLjUzMjIzMTAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYxLTEzMjQzOTMzNTAxLjUyODg0MzAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYwLTEzMjQzOTMzNTAxLjUyMDI5ODAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYwLTEzMjQzOTMzNTAxLjUwNzM4MjAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODU5LTEzMjQzOTMzNTAxLjc3NTM1MDAw', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTUyNC0xMzIzNjA4NTMzMC4w', - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEtMTMyMzYwODUzMjIuMA==', - ], - }, - args: ['/etc/cron.daily/bsdmainutils'], - parent: { - name: 'run-parts', - pid: 13861, - entity_id: - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYzLTEzMjQzOTMzNTAxLjU1MzMwMzAw', - executable: '/bin/run-parts', - }, - name: 'bsdmainutils', - pid: 13863, - args_count: 1, - entity_id: - 'YjFlMzI5OGUtMTBiZS00MDMyLWIxZWUtNWE0Y2JiMjgwYWExLTEzODYzLTEzMjQzOTMzNTAxLjU1ODEyMDAw', - command_line: '/etc/cron.daily/bsdmainutils', - executable: '/etc/cron.daily/bsdmainutils', - hash: { - sha1: 'fd24f1f3986e5527e804c4dccddee29ff42cb682', - sha256: - 'a68002bf1dc9f42a150087b00437448a46f7cae6755ecddca70a6d3c9d20a14b', - md5: '559387f792462a62e3efb1d573e38d11', - }, - }, - message: 'Endpoint process event', + fields: { + 'process.args': ['/etc/cron.daily/bsdmainutils'], + 'process.name': ['bsdmainutils'], + 'host.name': ['siem-kibana'], + 'user.name': ['root'], + 'user.id': [0], '@timestamp': '2020-09-07T06:25:01.155812000Z', - ecs: { - version: '1.5.0', - }, - data_stream: { - namespace: 'default', - type: 'logs', - dataset: 'endpoint.events.process', - }, - elastic: { - agent: { - id: 'ebee9a13-9ae3-4a55-9cb7-72ddf053055f', - }, - }, - host: { - hostname: 'siem-kibana', - os: { - Ext: { - variant: 'Debian', - }, - kernel: '4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27)', - name: 'Linux', - family: 'debian', - version: '9', - platform: 'debian', - full: 'Debian 9', - }, - ip: ['127.0.0.1', '::1', '10.142.0.7', 'fe80::4001:aff:fe8e:7'], - name: 'siem-kibana', - id: 'e50acb49-820b-c60a-392d-2ef75f276301', - mac: ['42:01:0a:8e:00:07'], - architecture: 'x86_64', - }, - event: { - sequence: 197063, - ingested: '2020-09-07T06:26:44.477164Z', - created: '2020-09-07T06:25:01.155812000Z', - kind: 'event', - module: 'endpoint', - action: 'exec', - id: 'Lp6oofT0fzv0Auzq+++/kwCZ', - category: ['process'], - type: ['start'], - dataset: 'endpoint.events.process', - }, - user: { - Ext: { - real: { - name: 'root', - id: 0, - }, - }, - name: 'root', - id: 0, - }, - group: { - Ext: { - real: { - name: 'root', - id: 0, - }, - }, - name: 'root', - id: 0, - }, }, }, ], @@ -4063,7 +1315,6 @@ export const formattedSearchStrategyResponse = { }, ], user: { - id: [], name: ['SYSTEM'], }, }, @@ -4091,7 +1342,6 @@ export const formattedSearchStrategyResponse = { }, ], user: { - id: [], name: ['SYSTEM'], }, }, @@ -4115,7 +1365,6 @@ export const formattedSearchStrategyResponse = { }, ], user: { - id: [], name: ['SYSTEM'], }, }, @@ -4139,7 +1388,6 @@ export const formattedSearchStrategyResponse = { }, ], user: { - id: [], name: ['SYSTEM'], }, }, @@ -4166,7 +1414,6 @@ export const formattedSearchStrategyResponse = { }, ], user: { - id: [], name: ['SYSTEM'], }, }, @@ -4190,7 +1437,6 @@ export const formattedSearchStrategyResponse = { }, ], user: { - id: [], name: ['SYSTEM'], }, }, @@ -4214,7 +1460,6 @@ export const formattedSearchStrategyResponse = { }, ], user: { - id: [], name: ['NETWORK SERVICE'], }, }, @@ -4238,7 +1483,6 @@ export const formattedSearchStrategyResponse = { }, ], user: { - id: [], name: ['SYSTEM'], }, }, @@ -4326,13 +1570,37 @@ export const formattedSearchStrategyResponse = { top_hits: { size: 1, sort: [{ '@timestamp': { order: 'desc' } }], - _source: ['process.args', 'process.name', 'user.id', 'user.name'], + _source: false, + fields: [ + 'process.args', + 'process.name', + 'user.id', + 'user.name', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], }, }, host_count: { cardinality: { field: 'host.name' } }, hosts: { terms: { field: 'host.name' }, - aggregations: { host: { top_hits: { size: 1, _source: [] } } }, + aggregations: { + host: { + top_hits: { + size: 1, + _source: false, + fields: [ + 'host.name', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], + }, + }, + }, }, }, }, @@ -4417,6 +1685,7 @@ export const formattedSearchStrategyResponse = { ], }, }, + _source: false, }, size: 0, track_total_hits: false, @@ -4461,13 +1730,37 @@ export const expectedDsl = { top_hits: { size: 1, sort: [{ '@timestamp': { order: 'desc' } }], - _source: ['process.args', 'process.name', 'user.id', 'user.name'], + _source: false, + fields: [ + 'process.args', + 'process.name', + 'user.id', + 'user.name', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], }, }, host_count: { cardinality: { field: 'host.name' } }, hosts: { terms: { field: 'host.name' }, - aggregations: { host: { top_hits: { size: 1, _source: [] } } }, + aggregations: { + host: { + top_hits: { + size: 1, + _source: false, + fields: [ + 'host.name', + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], + }, + }, + }, }, }, }, @@ -4552,6 +1845,7 @@ export const expectedDsl = { ], }, }, + _source: false, }, size: 0, track_total_hits: false, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts index e87e344e22eca..d7ed7caf0f782 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts @@ -13,7 +13,7 @@ import { userFieldsMap, } from '../../../../../../../common/ecs/ecs_fields'; import { RequestOptionsPaginated } from '../../../../../../../common/search_strategy/security_solution'; -import { uncommonProcessesFields } from '../helpers'; +import { UNCOMMON_PROCESSES_FIELDS } from '../helpers'; export const buildQuery = ({ defaultIndex, @@ -21,11 +21,11 @@ export const buildQuery = ({ pagination: { querySize }, timerange: { from, to }, }: RequestOptionsPaginated) => { - const processUserFields = reduceFields(uncommonProcessesFields, { + const processUserFields = reduceFields(UNCOMMON_PROCESSES_FIELDS, { ...processFieldsMap, ...userFieldsMap, }) as string[]; - const hostFields = reduceFields(uncommonProcessesFields, hostFieldsMap) as string[]; + const hostFields = reduceFields(UNCOMMON_PROCESSES_FIELDS, hostFieldsMap) as string[]; const filter = [ ...createQueryFilterClauses(filterQuery), { @@ -75,7 +75,14 @@ export const buildQuery = ({ top_hits: { size: 1, sort: [{ '@timestamp': { order: 'desc' as const } }], - _source: processUserFields, + _source: false, + fields: [ + ...processUserFields, + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], }, }, host_count: { @@ -91,7 +98,14 @@ export const buildQuery = ({ host: { top_hits: { size: 1, - _source: hostFields, + _source: false, + fields: [ + ...hostFields, + { + field: '@timestamp', + format: 'strict_date_optional_time', + }, + ], }, }, }, @@ -218,6 +232,7 @@ export const buildQuery = ({ filter, }, }, + _source: false, }, size: 0, track_total_hits: false, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts index 1bd80dca6c232..0492a66700b6e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts @@ -32,11 +32,9 @@ describe('helpers', () => { _type: 'type-1', _id: 'id-1', _score: 0, - _source: { - host: { - name: ['host-1'], - id: ['host-id-1'], - }, + fields: { + 'host.id': ['host-id-1'], + 'host.name': ['host-1'], }, }, ], @@ -72,11 +70,9 @@ describe('helpers', () => { _type: 'type-1', _id: 'id-1', _score: 0, - _source: { - host: { - name: ['host-1'], - id: ['host-id-1'], - }, + fields: { + 'host.id': ['host-id-1'], + 'host.name': ['host-1'], }, }, ], @@ -95,11 +91,9 @@ describe('helpers', () => { _type: 'type-2', _id: 'id-2', _score: 0, - _source: { - host: { - name: ['host-2'], - id: ['host-id-2'], - }, + fields: { + 'host.id': ['host-id-2'], + 'host.name': ['host-2'], }, }, ], @@ -135,8 +129,7 @@ describe('helpers', () => { _type: 'type-9', _id: 'id-9', _score: 0, - _source: { - // @ts-expect-error ts doesn't like seeing the object written this way, but sometimes this is the data we get! + fields: { 'host.id': ['host-id-9'], 'host.name': ['host-9'], }, @@ -197,40 +190,17 @@ describe('helpers', () => { { id: ['host-id-1'], name: ['host-name-1'] }, { id: ['host-id-1'], name: ['host-name-1'] }, ], - _source: { - '@timestamp': 'time', - process: { - name: ['process-1'], - title: ['title-1'], - }, + fields: { + '@timestamp': ['time'], + 'process.name': ['process-1'], + 'process.args': ['args-1'], }, cursor: 'cursor-1', sort: [0], }; - test('it formats a uncommon process data with a source of name correctly', () => { - const fields: readonly string[] = ['process.name']; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: HostsUncommonProcessesEdges = { - cursor: { tiebreaker: null, value: 'cursor-1' }, - node: { - _id: 'id-123', - hosts: [ - { id: ['host-id-1'], name: ['host-name-1'] }, - { id: ['host-id-1'], name: ['host-name-1'] }, - ], - process: { - name: ['process-1'], - }, - instances: 100, - }, - }; - expect(data).toEqual(expected); - }); - test('it formats a uncommon process data with a source of name and title correctly', () => { - const fields: readonly string[] = ['process.name', 'process.title']; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); + const data = formatUncommonProcessesData(hit, processFieldsMap); const expected: HostsUncommonProcessesEdges = { cursor: { tiebreaker: null, value: 'cursor-1' }, node: { @@ -242,23 +212,36 @@ describe('helpers', () => { instances: 100, process: { name: ['process-1'], - title: ['title-1'], + args: ['args-1'], }, }, }; expect(data).toEqual(expected); }); - test('it formats a uncommon process data without any data if fields is empty', () => { - const fields: readonly string[] = []; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); + test('it formats a uncommon process data without any data if fields map is empty', () => { + const emptyHit: HostsUncommonProcessHit = { + _index: 'index-123', + _type: 'type-123', + _id: 'id-123', + _score: 10, + total: { + value: 0, + relation: 'eq', + }, + host: [], + fields: {}, + cursor: '', + sort: [0], + }; + const data = formatUncommonProcessesData(emptyHit, processFieldsMap); const expected: HostsUncommonProcessesEdges = { cursor: { tiebreaker: null, value: '', }, node: { - _id: '', + _id: 'id-123', hosts: [], instances: 0, process: {}, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index 1c1e2111f3771..b5188c36fb8aa 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -8,23 +8,22 @@ import { get } from 'lodash/fp'; import { set } from '@elastic/safer-lodash-set/fp'; -import { mergeFieldsWithHit } from '../../../../../utils/build_query'; import { ProcessHits, HostsUncommonProcessesEdges, HostsUncommonProcessHit, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; -import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { HostHits } from '../../../../../../common/search_strategy'; +import { getFlattenedFields } from '../../../../helpers/get_flattened_fields'; -export const uncommonProcessesFields = [ +export const UNCOMMON_PROCESSES_FIELDS = [ '_id', 'instances', 'process.args', 'process.name', 'user.id', 'user.name', - 'hosts.name', + 'host.name', ]; export const getHits = ( @@ -35,13 +34,22 @@ export const getHits = ( _index: bucket.process.hits.hits[0]._index, _type: bucket.process.hits.hits[0]._type, _score: bucket.process.hits.hits[0]._score, - _source: bucket.process.hits.hits[0]._source, + fields: bucket.process.hits.hits[0].fields, sort: bucket.process.hits.hits[0].sort, cursor: bucket.process.hits.hits[0].cursor, total: bucket.process.hits.total, host: getHosts(bucket.hosts.buckets), })); +export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }>) => + buckets.map((bucket) => { + const fields = get('host.hits.hits[0].fields', bucket); + return { + id: [bucket.key], + name: get('host.name', fields), + }; + }); + export interface UncommonProcessBucket { key: string; hosts: { @@ -50,54 +58,37 @@ export interface UncommonProcessBucket { process: ProcessHits; } -export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }>) => - buckets.map((bucket) => { - const source = get('host.hits.hits[0]._source', bucket); - return { - id: [bucket.key], - name: get('host.name', source), - }; - }); - export const formatUncommonProcessesData = ( - fields: readonly string[], hit: HostsUncommonProcessHit, fieldMap: Readonly> -): HostsUncommonProcessesEdges => - fields.reduce( - (flattenedFields, fieldName) => { - const instancesCount = typeof hit.total === 'number' ? hit.total : hit.total.value; - flattenedFields.node._id = hit._id; - flattenedFields.node.instances = instancesCount; - flattenedFields.node.hosts = hit.host; - - if (hit.cursor) { - flattenedFields.cursor.value = hit.cursor; - } - - const mergedResult = mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit); - let fieldPath = `node.${fieldName}`; - let fieldValue = get(fieldPath, mergedResult); - if (fieldPath === 'node.hosts.name') { - fieldPath = `node.hosts.0.name`; - fieldValue = get(fieldPath, mergedResult); - } - return set( - fieldPath, - toObjectArrayOfStrings(fieldValue).map(({ str }) => str), - mergedResult - ); +): HostsUncommonProcessesEdges => { + let flattenedFields = { + node: { + _id: '', + instances: 0, + process: {}, + hosts: [{}], + }, + cursor: { + value: '', + tiebreaker: null, }, - { - node: { - _id: '', - instances: 0, - process: {}, - hosts: [], - }, - cursor: { - value: '', - tiebreaker: null, - }, - } + }; + const instancesCount = typeof hit.total === 'number' ? hit.total : hit.total.value; + const processFlattenedFields = getFlattenedFields( + UNCOMMON_PROCESSES_FIELDS, + hit.fields, + fieldMap ); + + if (Object.keys(processFlattenedFields).length > 0) { + flattenedFields = set('node', processFlattenedFields, flattenedFields); + } + flattenedFields.node._id = hit._id; + flattenedFields.node.instances = instancesCount; + flattenedFields.node.hosts = hit.host; + if (hit.cursor) { + flattenedFields.cursor.value = hit.cursor; + } + return flattenedFields; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts index ae46adca25680..117d65f10d74c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts @@ -21,7 +21,7 @@ import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { buildQuery } from './dsl/query.dsl'; -import { formatUncommonProcessesData, getHits, uncommonProcessesFields } from './helpers'; +import { formatUncommonProcessesData, getHits } from './helpers'; export const uncommonProcesses: SecuritySolutionFactory = { buildDsl: (options: HostsUncommonProcessesRequestOptions) => { @@ -40,7 +40,7 @@ export const uncommonProcesses: SecuritySolutionFactory - formatUncommonProcessesData(uncommonProcessesFields, hit, { + formatUncommonProcessesData(hit, { ...processFieldsMap, ...userFieldsMap, }) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts index 884bf606d2f90..c19d4ed5c3d1f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { EsQueryAlertActionContext, addMessages } from './action_context'; -import { EsQueryAlertParamsSchema } from './alert_type_params'; -import { OnlyEsQueryAlertParams } from './types'; +import { EsQueryRuleActionContext, addMessages } from './action_context'; +import { EsQueryRuleParamsSchema } from './rule_type_params'; +import { OnlyEsQueryRuleParams } from './types'; describe('ActionContext', () => { it('generates expected properties', async () => { - const params = EsQueryAlertParamsSchema.validate({ + const params = EsQueryRuleParamsSchema.validate({ index: ['[index]'], timeField: '[timeField]', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -21,18 +21,18 @@ describe('ActionContext', () => { thresholdComparator: '>', threshold: [4], searchType: 'esQuery', - }) as OnlyEsQueryAlertParams; - const base: EsQueryAlertActionContext = { + }) as OnlyEsQueryRuleParams; + const base: EsQueryRuleActionContext = { date: '2020-01-01T00:00:00.000Z', value: 42, conditions: 'count greater than 4', hits: [], link: 'link-mock', }; - const context = addMessages({ name: '[alert-name]' }, base, params); - expect(context.title).toMatchInlineSnapshot(`"alert '[alert-name]' matched query"`); + const context = addMessages({ name: '[rule-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"rule '[rule-name]' matched query"`); expect(context.message).toEqual( - `alert '[alert-name]' is active: + `rule '[rule-name]' is active: - Value: 42 - Conditions Met: count greater than 4 over 5m @@ -41,8 +41,39 @@ describe('ActionContext', () => { ); }); + it('generates expected properties when isRecovered is true', async () => { + const params = EsQueryRuleParamsSchema.validate({ + index: ['[index]'], + timeField: '[timeField]', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [4], + searchType: 'esQuery', + }) as OnlyEsQueryRuleParams; + const base: EsQueryRuleActionContext = { + date: '2020-01-01T00:00:00.000Z', + value: 42, + conditions: 'count not greater than 4', + hits: [], + link: 'link-mock', + }; + const context = addMessages({ name: '[rule-name]' }, base, params, true); + expect(context.title).toMatchInlineSnapshot(`"rule '[rule-name]' recovered"`); + expect(context.message).toEqual( + `rule '[rule-name]' is recovered: + +- Value: 42 +- Conditions Met: count not greater than 4 over 5m +- Timestamp: 2020-01-01T00:00:00.000Z +- Link: link-mock` + ); + }); + it('generates expected properties if comparator is between', async () => { - const params = EsQueryAlertParamsSchema.validate({ + const params = EsQueryRuleParamsSchema.validate({ index: ['[index]'], timeField: '[timeField]', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -52,18 +83,18 @@ describe('ActionContext', () => { thresholdComparator: 'between', threshold: [4, 5], searchType: 'esQuery', - }) as OnlyEsQueryAlertParams; - const base: EsQueryAlertActionContext = { + }) as OnlyEsQueryRuleParams; + const base: EsQueryRuleActionContext = { date: '2020-01-01T00:00:00.000Z', value: 4, conditions: 'count between 4 and 5', hits: [], link: 'link-mock', }; - const context = addMessages({ name: '[alert-name]' }, base, params); - expect(context.title).toMatchInlineSnapshot(`"alert '[alert-name]' matched query"`); + const context = addMessages({ name: '[rule-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"rule '[rule-name]' matched query"`); expect(context.message).toEqual( - `alert '[alert-name]' is active: + `rule '[rule-name]' is active: - Value: 4 - Conditions Met: count between 4 and 5 over 5m diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts index 68367e5ec8104..f25b35c6c63d6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -8,21 +8,21 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { RuleExecutorOptions, AlertInstanceContext } from '@kbn/alerting-plugin/server'; -import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; +import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types'; -// alert type context provided to actions +// rule type context provided to actions -type AlertInfo = Pick; +type RuleInfo = Pick; -export interface ActionContext extends EsQueryAlertActionContext { +export interface ActionContext extends EsQueryRuleActionContext { // a short pre-constructed message which may be used in an action field title: string; // a longer pre-constructed message which may be used in an action field message: string; } -export interface EsQueryAlertActionContext extends AlertInstanceContext { - // the date the alert was run as an ISO date +export interface EsQueryRuleActionContext extends AlertInstanceContext { + // the date the rule was run as an ISO date date: string; // the value that met the threshold value: number; @@ -30,38 +30,41 @@ export interface EsQueryAlertActionContext extends AlertInstanceContext { conditions: string; // query matches hits: estypes.SearchHit[]; - // a link to see records that triggered the alert for Discover alert - // a link which navigates to stack management in case of Elastic query alert + // a link to see records that triggered the rule for Discover rule + // a link which navigates to stack management in case of Elastic query rule link: string; } export function addMessages( - alertInfo: AlertInfo, - baseContext: EsQueryAlertActionContext, - params: OnlyEsQueryAlertParams | OnlySearchSourceAlertParams + ruleInfo: RuleInfo, + baseContext: EsQueryRuleActionContext, + params: OnlyEsQueryRuleParams | OnlySearchSourceRuleParams, + isRecovered: boolean = false ): ActionContext { const title = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle', { - defaultMessage: `alert '{name}' matched query`, + defaultMessage: `rule '{name}' {verb}`, values: { - name: alertInfo.name, + name: ruleInfo.name, + verb: isRecovered ? 'recovered' : 'matched query', }, }); const window = `${params.timeWindowSize}${params.timeWindowUnit}`; const message = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextMessageDescription', { - defaultMessage: `alert '{name}' is active: + defaultMessage: `rule '{name}' is {verb}: - Value: {value} - Conditions Met: {conditions} over {window} - Timestamp: {date} - Link: {link}`, values: { - name: alertInfo.name, + name: ruleInfo.name, value: baseContext.value, conditions: baseContext.conditions, window, date: baseContext.date, link: baseContext.link, + verb: isRecovered ? 'recovered' : 'active', }, }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts index 7b4cc7521654b..97b02a4dc723e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts @@ -5,8 +5,14 @@ * 2.0. */ -import { getSearchParams, getValidTimefieldSort, tryToParseAsDate } from './executor'; -import { OnlyEsQueryAlertParams } from './types'; +import { + getSearchParams, + getValidTimefieldSort, + tryToParseAsDate, + getContextConditionsDescription, +} from './executor'; +import { OnlyEsQueryRuleParams } from './types'; +import { Comparator } from '../../../common/comparator_types'; describe('es_query executor', () => { const defaultProps = { @@ -49,13 +55,13 @@ describe('es_query executor', () => { describe('getSearchParams', () => { it('should return search params correctly', () => { - const result = getSearchParams(defaultProps as OnlyEsQueryAlertParams); + const result = getSearchParams(defaultProps as OnlyEsQueryRuleParams); expect(result.parsedQuery.query).toBe('test-query'); }); it('should throw invalid query error', () => { expect(() => - getSearchParams({ ...defaultProps, esQuery: '' } as OnlyEsQueryAlertParams) + getSearchParams({ ...defaultProps, esQuery: '' } as OnlyEsQueryRuleParams) ).toThrow('invalid query specified: "" - query must be JSON'); }); @@ -64,7 +70,7 @@ describe('es_query executor', () => { getSearchParams({ ...defaultProps, esQuery: '{ "someProperty": "test-query" }', - } as OnlyEsQueryAlertParams) + } as OnlyEsQueryRuleParams) ).toThrow('invalid query specified: "{ "someProperty": "test-query" }" - query must be JSON'); }); @@ -74,8 +80,25 @@ describe('es_query executor', () => { ...defaultProps, timeWindowSize: 5, timeWindowUnit: 'r', - } as OnlyEsQueryAlertParams) + } as OnlyEsQueryRuleParams) ).toThrow('invalid format for windowSize: "5r"'); }); }); + + describe('getContextConditionsDescription', () => { + it('should return conditions correctly', () => { + const result = getContextConditionsDescription(Comparator.GT, [10]); + expect(result).toBe(`Number of matching documents is greater than 10`); + }); + + it('should return conditions correctly when isRecovered is true', () => { + const result = getContextConditionsDescription(Comparator.GT, [10], true); + expect(result).toBe(`Number of matching documents is NOT greater than 10`); + }); + + it('should return conditions correctly when multiple thresholds provided', () => { + const result = getContextConditionsDescription(Comparator.BETWEEN, [10, 20], true); + expect(result).toBe(`Number of matching documents is NOT between 10 and 20`); + }); + }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts index 6e47c5f471d88..5f33eeb0af845 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts @@ -8,23 +8,23 @@ import { sha256 } from 'js-sha256'; import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger } from '@kbn/core/server'; import { parseDuration } from '@kbn/alerting-plugin/server'; -import { addMessages, EsQueryAlertActionContext } from './action_context'; +import { addMessages, EsQueryRuleActionContext } from './action_context'; import { ComparatorFns, getHumanReadableComparator } from '../lib'; -import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; +import { ExecutorOptions, OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types'; import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; import { fetchEsQuery } from './lib/fetch_es_query'; -import { EsQueryAlertParams } from './alert_type_params'; +import { EsQueryRuleParams } from './rule_type_params'; import { fetchSearchSourceQuery } from './lib/fetch_search_source_query'; import { Comparator } from '../../../common/comparator_types'; -import { isEsQueryAlert } from './util'; +import { isEsQueryRule } from './util'; export async function executor( logger: Logger, core: CoreSetup, - options: ExecutorOptions + options: ExecutorOptions ) { - const esQueryAlert = isEsQueryAlert(options.params.searchType); - const { alertId, name, services, params, state } = options; + const esQueryRule = isEsQueryRule(options.params.searchType); + const { alertId: ruleId, name, services, params, state, spaceId } = options; const { alertFactory, scopedClusterClient, searchSourceClient } = services; const currentTimestamp = new Date().toISOString(); const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; @@ -35,51 +35,49 @@ export async function executor( } let latestTimestamp: string | undefined = tryToParseAsDate(state.latestTimestamp); - // During each alert execution, we run the configured query, get a hit count + // During each rule execution, we run the configured query, get a hit count // (hits.total) and retrieve up to params.size hits. We // evaluate the threshold condition using the value of hits.total. If the threshold // condition is met, the hits are counted toward the query match and we update - // the alert state with the timestamp of the latest hit. In the next execution - // of the alert, the latestTimestamp will be used to gate the query in order to + // the rule state with the timestamp of the latest hit. In the next execution + // of the rule, the latestTimestamp will be used to gate the query in order to // avoid counting a document multiple times. - const { numMatches, searchResult, dateStart, dateEnd } = esQueryAlert - ? await fetchEsQuery(alertId, name, params as OnlyEsQueryAlertParams, latestTimestamp, { + const { numMatches, searchResult, dateStart, dateEnd } = esQueryRule + ? await fetchEsQuery(ruleId, name, params as OnlyEsQueryRuleParams, latestTimestamp, { scopedClusterClient, logger, }) - : await fetchSearchSourceQuery( - alertId, - params as OnlySearchSourceAlertParams, - latestTimestamp, - { searchSourceClient, logger } - ); - - // apply the alert condition + : await fetchSearchSourceQuery(ruleId, params as OnlySearchSourceRuleParams, latestTimestamp, { + searchSourceClient, + logger, + }); + + // apply the rule condition const conditionMet = compareFn(numMatches, params.threshold); + const base = publicBaseUrl; + const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const link = esQueryRule + ? `${base}${spacePrefix}/app/management/insightsAndAlerting/triggersActions/rule/${ruleId}` + : `${base}${spacePrefix}/app/discover#/viewAlert/${ruleId}?from=${dateStart}&to=${dateEnd}&checksum=${getChecksum( + params as OnlyEsQueryRuleParams + )}`; + const baseContext: Omit = { + title: name, + date: currentTimestamp, + value: numMatches, + hits: searchResult.hits.hits, + link, + }; + if (conditionMet) { - const base = publicBaseUrl; - const link = esQueryAlert - ? `${base}/app/management/insightsAndAlerting/triggersActions/rule/${alertId}` - : `${base}/app/discover#/viewAlert/${alertId}?from=${dateStart}&to=${dateEnd}&checksum=${getChecksum( - params - )}`; - - const conditions = getContextConditionsDescription( - params.thresholdComparator, - params.threshold - ); - const baseContext: EsQueryAlertActionContext = { - title: name, - date: currentTimestamp, - value: numMatches, - conditions, - hits: searchResult.hits.hits, - link, - }; - - const actionContext = addMessages(options, baseContext, params); + const baseActiveContext: EsQueryRuleActionContext = { + ...baseContext, + conditions: getContextConditionsDescription(params.thresholdComparator, params.threshold), + } as EsQueryRuleActionContext; + + const actionContext = addMessages(options, baseActiveContext, params); const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); alertInstance // store the params we would need to recreate the query that led to this alert instance @@ -95,6 +93,20 @@ export async function executor( } } + const { getRecoveredAlerts } = alertFactory.done(); + for (const alert of getRecoveredAlerts()) { + const baseRecoveryContext: EsQueryRuleActionContext = { + ...baseContext, + conditions: getContextConditionsDescription( + params.thresholdComparator, + params.threshold, + true + ), + } as EsQueryRuleActionContext; + const recoveryContext = addMessages(options, baseRecoveryContext, params, true); + alert.setContext(recoveryContext); + } + return { latestTimestamp }; } @@ -116,7 +128,7 @@ function getInvalidQueryError(query: string) { }); } -export function getSearchParams(queryParams: OnlyEsQueryAlertParams) { +export function getSearchParams(queryParams: OnlyEsQueryRuleParams) { const date = Date.now(); const { esQuery, timeWindowSize, timeWindowUnit } = queryParams; @@ -163,7 +175,7 @@ export function tryToParseAsDate(sortValue?: string | number | null): undefined } } -export function getChecksum(params: EsQueryAlertParams) { +export function getChecksum(params: OnlyEsQueryRuleParams) { return sha256.create().update(JSON.stringify(params)); } @@ -176,12 +188,17 @@ export function getInvalidComparatorError(comparator: string) { }); } -export function getContextConditionsDescription(comparator: Comparator, threshold: number[]) { +export function getContextConditionsDescription( + comparator: Comparator, + threshold: number[], + isRecovered: boolean = false +) { return i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', { - defaultMessage: 'Number of matching documents is {thresholdComparator} {threshold}', + defaultMessage: 'Number of matching documents is {negation}{thresholdComparator} {threshold}', values: { thresholdComparator: getHumanReadableComparator(comparator), threshold: threshold.join(' and '), + negation: isRecovered ? 'NOT ' : '', }, }); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts index 82f8297a85bb5..54bfabdf49ad6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts @@ -7,7 +7,7 @@ import { CoreSetup, Logger } from '@kbn/core/server'; import { AlertingSetup } from '../../types'; -import { getAlertType } from './alert_type'; +import { getRuleType } from './rule_type'; interface RegisterParams { logger: Logger; @@ -17,5 +17,5 @@ interface RegisterParams { export function register(params: RegisterParams) { const { logger, alerting, core } = params; - alerting.registerType(getAlertType(logger, core)); + alerting.registerType(getRuleType(logger, core)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts index b4d74412b7f83..97acd15416689 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts @@ -6,18 +6,18 @@ */ import { IScopedClusterClient, Logger } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { OnlyEsQueryAlertParams } from '../types'; +import { OnlyEsQueryRuleParams } from '../types'; import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query'; import { ES_QUERY_ID } from '../constants'; import { getSearchParams } from './get_search_params'; /** - * Fetching matching documents for a given alert from elasticsearch by a given index and query + * Fetching matching documents for a given rule from elasticsearch by a given index and query */ export async function fetchEsQuery( - alertId: string, + ruleId: string, name: string, - params: OnlyEsQueryAlertParams, + params: OnlyEsQueryRuleParams, timestamp: string | undefined, services: { scopedClusterClient: IScopedClusterClient; @@ -70,14 +70,12 @@ export async function fetchEsQuery( track_total_hits: true, }); - logger.debug( - `es query alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}` - ); + logger.debug(`es query rule ${ES_QUERY_ID}:${ruleId} "${name}" query - ${JSON.stringify(query)}`); const { body: searchResult } = await esClient.search(query, { meta: true }); logger.debug( - ` es query alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` + ` es query rule ${ES_QUERY_ID}:${ruleId} "${name}" result - ${JSON.stringify(searchResult)}` ); return { numMatches: (searchResult.hits.total as estypes.SearchTotalHits).value, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts index 48082f565afb3..6b177d1b94a86 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { OnlySearchSourceAlertParams } from '../types'; +import { OnlySearchSourceRuleParams } from '../types'; import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { updateSearchSource } from './fetch_search_source_query'; import { stubbedSavedObjectIndexPattern } from '@kbn/data-views-plugin/common/data_view.stub'; @@ -29,7 +29,7 @@ const createDataView = () => { }); }; -const defaultParams: OnlySearchSourceAlertParams = { +const defaultParams: OnlySearchSourceRuleParams = { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts index 66e5ae8023a47..e3922adf1e15c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts @@ -12,11 +12,11 @@ import { ISearchStartSearchSource, SortDirection, } from '@kbn/data-plugin/common'; -import { OnlySearchSourceAlertParams } from '../types'; +import { OnlySearchSourceRuleParams } from '../types'; export async function fetchSearchSourceQuery( - alertId: string, - params: OnlySearchSourceAlertParams, + ruleId: string, + params: OnlySearchSourceRuleParams, latestTimestamp: string | undefined, services: { logger: Logger; @@ -34,7 +34,7 @@ export async function fetchSearchSourceQuery( ); logger.debug( - `search source query alert (${alertId}) query: ${JSON.stringify( + `search source query rule (${ruleId}) query: ${JSON.stringify( searchSource.getSearchRequestBody() )}` ); @@ -51,7 +51,7 @@ export async function fetchSearchSourceQuery( export function updateSearchSource( searchSource: ISearchSource, - params: OnlySearchSourceAlertParams, + params: OnlySearchSourceRuleParams, latestTimestamp: string | undefined ) { const index = searchSource.getField('index'); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts index 29bb7ad544804..126ddb3009287 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts @@ -6,9 +6,9 @@ */ import { i18n } from '@kbn/i18n'; import { parseDuration } from '@kbn/alerting-plugin/common'; -import { OnlyEsQueryAlertParams } from '../types'; +import { OnlyEsQueryRuleParams } from '../types'; -export function getSearchParams(queryParams: OnlyEsQueryAlertParams) { +export function getSearchParams(queryParams: OnlyEsQueryRuleParams) { const date = Date.now(); const { esQuery, timeWindowSize, timeWindowUnit } = queryParams; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts similarity index 73% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts index 3304ca5e902f7..8e54a9ac9da8f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.test.ts @@ -14,29 +14,29 @@ import { AlertInstanceMock, } from '@kbn/alerting-plugin/server/mocks'; import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { getAlertType } from './alert_type'; -import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; +import { getRuleType } from './rule_type'; +import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params'; import { ActionContext } from './action_context'; import { ESSearchResponse, ESSearchRequest } from '@kbn/core/types/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '@kbn/core/server/elasticsearch/client/mocks'; import { coreMock } from '@kbn/core/server/mocks'; import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; -import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; +import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { Comparator } from '../../../common/comparator_types'; const logger = loggingSystemMock.create().get(); const coreSetup = coreMock.createSetup(); -const alertType = getAlertType(logger, coreSetup); +const ruleType = getRuleType(logger, coreSetup); -describe('alertType', () => { - it('alert type creation structure is the expected value', async () => { - expect(alertType.id).toBe('.es-query'); - expect(alertType.name).toBe('Elasticsearch query'); - expect(alertType.actionGroups).toEqual([{ id: 'query matched', name: 'Query matched' }]); +describe('ruleType', () => { + it('rule type creation structure is the expected value', async () => { + expect(ruleType.id).toBe('.es-query'); + expect(ruleType.name).toBe('Elasticsearch query'); + expect(ruleType.actionGroups).toEqual([{ id: 'query matched', name: 'Query matched' }]); - expect(alertType.actionVariables).toMatchInlineSnapshot(` + expect(ruleType.actionVariables).toMatchInlineSnapshot(` Object { "context": Array [ Object { @@ -101,7 +101,7 @@ describe('alertType', () => { describe('elasticsearch query', () => { it('validator succeeds with valid es query params', async () => { - const params: Partial> = { + const params: Partial> = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -113,14 +113,14 @@ describe('alertType', () => { searchType: 'esQuery', }; - expect(alertType.validate?.params?.validate(params)).toBeTruthy(); + expect(ruleType.validate?.params?.validate(params)).toBeTruthy(); }); it('validator fails with invalid es query params - threshold', async () => { - const paramsSchema = alertType.validate?.params; + const paramsSchema = ruleType.validate?.params; if (!paramsSchema) throw new Error('params validator not set'); - const params: Partial> = { + const params: Partial> = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -137,8 +137,8 @@ describe('alertType', () => { ); }); - it('alert executor handles no documents returned by ES', async () => { - const params: OnlyEsQueryAlertParams = { + it('rule executor handles no documents returned by ES', async () => { + const params: OnlyEsQueryRuleParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -149,16 +149,16 @@ describe('alertType', () => { threshold: [0], searchType: 'esQuery', }; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); const searchResult: ESSearchResponse = generateResults([]); - alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) ); - const result = await invokeExecutor({ params, alertServices }); + const result = await invokeExecutor({ params, ruleServices }); - expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); + expect(ruleServices.alertFactory.create).not.toHaveBeenCalled(); expect(result).toMatchInlineSnapshot(` Object { @@ -167,8 +167,8 @@ describe('alertType', () => { `); }); - it('alert executor returns the latestTimestamp of the newest detected document', async () => { - const params: OnlyEsQueryAlertParams = { + it('rule executor returns the latestTimestamp of the newest detected document', async () => { + const params: OnlyEsQueryRuleParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -179,7 +179,7 @@ describe('alertType', () => { threshold: [0], searchType: 'esQuery', }; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); const newestDocumentTimestamp = Date.now(); @@ -194,14 +194,14 @@ describe('alertType', () => { 'time-field': newestDocumentTimestamp - 2000, }, ]); - alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) ); - const result = await invokeExecutor({ params, alertServices }); + const result = await invokeExecutor({ params, ruleServices }); - expect(alertServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId); - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + expect(ruleServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId); + const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -213,8 +213,8 @@ describe('alertType', () => { }); }); - it('alert executor correctly handles numeric time fields that were stored by legacy rules prior to v7.12.1', async () => { - const params: OnlyEsQueryAlertParams = { + it('rule executor correctly handles numeric time fields that were stored by legacy rules prior to v7.12.1', async () => { + const params: OnlyEsQueryRuleParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -225,12 +225,12 @@ describe('alertType', () => { threshold: [0], searchType: 'esQuery', }; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); const previousTimestamp = Date.now(); const newestDocumentTimestamp = previousTimestamp + 1000; - alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults([ { @@ -242,14 +242,14 @@ describe('alertType', () => { const result = await invokeExecutor({ params, - alertServices, + ruleServices, state: { // @ts-expect-error previousTimestamp is numeric, but should be string (this was a bug prior to v7.12.1) latestTimestamp: previousTimestamp, }, }); - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward latestTimestamp: new Date(previousTimestamp).toISOString(), @@ -262,8 +262,8 @@ describe('alertType', () => { }); }); - it('alert executor ignores previous invalid latestTimestamp values stored by legacy rules prior to v7.12.1', async () => { - const params: OnlyEsQueryAlertParams = { + it('rule executor ignores previous invalid latestTimestamp values stored by legacy rules prior to v7.12.1', async () => { + const params: OnlyEsQueryRuleParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -274,11 +274,11 @@ describe('alertType', () => { threshold: [0], searchType: 'esQuery', }; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); const oldestDocumentTimestamp = Date.now(); - alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults([ { @@ -291,9 +291,9 @@ describe('alertType', () => { ) ); - const result = await invokeExecutor({ params, alertServices }); + const result = await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -305,8 +305,8 @@ describe('alertType', () => { }); }); - it('alert executor carries over the queried latestTimestamp in the alert state', async () => { - const params: OnlyEsQueryAlertParams = { + it('rule executor carries over the queried latestTimestamp in the rule state', async () => { + const params: OnlyEsQueryRuleParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -317,11 +317,11 @@ describe('alertType', () => { threshold: [0], searchType: 'esQuery', }; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); const oldestDocumentTimestamp = Date.now(); - alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults([ { @@ -331,9 +331,9 @@ describe('alertType', () => { ) ); - const result = await invokeExecutor({ params, alertServices }); + const result = await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -345,7 +345,7 @@ describe('alertType', () => { }); const newestDocumentTimestamp = oldestDocumentTimestamp + 5000; - alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults([ { @@ -360,12 +360,12 @@ describe('alertType', () => { const secondResult = await invokeExecutor({ params, - alertServices, - state: result as EsQueryAlertState, + ruleServices, + state: result as EsQueryRuleState, }); const existingInstance: AlertInstanceMock = - alertServices.alertFactory.create.mock.results[1].value; + ruleServices.alertFactory.create.mock.results[1].value; expect(existingInstance.replaceState).toHaveBeenCalledWith({ latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), dateStart: expect.any(String), @@ -377,8 +377,8 @@ describe('alertType', () => { }); }); - it('alert executor ignores tie breaker sort values', async () => { - const params: OnlyEsQueryAlertParams = { + it('rule executor ignores tie breaker sort values', async () => { + const params: OnlyEsQueryRuleParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -389,11 +389,11 @@ describe('alertType', () => { threshold: [0], searchType: 'esQuery', }; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); const oldestDocumentTimestamp = Date.now(); - alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults( [ @@ -409,9 +409,9 @@ describe('alertType', () => { ) ); - const result = await invokeExecutor({ params, alertServices }); + const result = await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -423,8 +423,8 @@ describe('alertType', () => { }); }); - it('alert executor ignores results with no sort values', async () => { - const params: OnlyEsQueryAlertParams = { + it('rule executor ignores results with no sort values', async () => { + const params: OnlyEsQueryRuleParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -435,11 +435,11 @@ describe('alertType', () => { threshold: [0], searchType: 'esQuery', }; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); const oldestDocumentTimestamp = Date.now(); - alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + ruleServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( generateResults( [ @@ -456,9 +456,9 @@ describe('alertType', () => { ) ); - const result = await invokeExecutor({ params, alertServices }); + const result = await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -495,7 +495,7 @@ describe('alertType', () => { }, ], }; - const defaultParams: OnlySearchSourceAlertParams = { + const defaultParams: OnlySearchSourceRuleParams = { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', @@ -510,12 +510,12 @@ describe('alertType', () => { }); it('validator succeeds with valid search source params', async () => { - expect(alertType.validate?.params?.validate(defaultParams)).toBeTruthy(); + expect(ruleType.validate?.params?.validate(defaultParams)).toBeTruthy(); }); it('validator fails with invalid search source params - esQuery provided', async () => { - const paramsSchema = alertType.validate?.params!; - const params: Partial> = { + const paramsSchema = ruleType.validate?.params!; + const params: Partial> = { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', @@ -530,10 +530,10 @@ describe('alertType', () => { ); }); - it('alert executor handles no documents returned by ES', async () => { + it('rule executor handles no documents returned by ES', async () => { const params = defaultParams; const searchResult: ESSearchResponse = generateResults([]); - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { if (name === 'index') { @@ -542,14 +542,14 @@ describe('alertType', () => { }); (searchSourceInstanceMock.fetch as jest.Mock).mockResolvedValueOnce(searchResult); - await invokeExecutor({ params, alertServices }); + await invokeExecutor({ params, ruleServices }); - expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); + expect(ruleServices.alertFactory.create).not.toHaveBeenCalled(); }); - it('alert executor throws an error when index does not have time field', async () => { + it('rule executor throws an error when index does not have time field', async () => { const params = defaultParams; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { if (name === 'index') { @@ -557,14 +557,14 @@ describe('alertType', () => { } }); - await expect(invokeExecutor({ params, alertServices })).rejects.toThrow( + await expect(invokeExecutor({ params, ruleServices })).rejects.toThrow( 'Invalid data view without timeFieldName.' ); }); - it('alert executor schedule actions when condition met', async () => { + it('rule executor schedule actions when condition met', async () => { const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] }; - const alertServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleServices: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { if (name === 'index') { @@ -576,9 +576,9 @@ describe('alertType', () => { hits: { total: 3, hits: [{}, {}, {}] }, }); - await invokeExecutor({ params, alertServices }); + await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; expect(instance.scheduleActions).toHaveBeenCalled(); }); }); @@ -625,24 +625,24 @@ function generateResults( async function invokeExecutor({ params, - alertServices, + ruleServices, state, }: { - params: OnlySearchSourceAlertParams | OnlyEsQueryAlertParams; - alertServices: RuleExecutorServicesMock; - state?: EsQueryAlertState; + params: OnlySearchSourceRuleParams | OnlyEsQueryRuleParams; + ruleServices: RuleExecutorServicesMock; + state?: EsQueryRuleState; }) { - return await alertType.executor({ + return await ruleType.executor({ alertId: uuid.v4(), executionId: uuid.v4(), startedAt: new Date(), previousStartedAt: new Date(), - services: alertServices as unknown as RuleExecutorServices< - EsQueryAlertState, + services: ruleServices as unknown as RuleExecutorServices< + EsQueryRuleState, ActionContext, typeof ActionGroupId >, - params: params as EsQueryAlertParams, + params: params as EsQueryRuleParams, state: { latestTimestamp: undefined, ...state, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts similarity index 88% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts index dfab69f445629..27e79e86fb3c3 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type.ts @@ -11,29 +11,29 @@ import { extractReferences, injectReferences } from '@kbn/data-plugin/common'; import { RuleType } from '../../types'; import { ActionContext } from './action_context'; import { - EsQueryAlertParams, - EsQueryAlertParamsExtractedParams, - EsQueryAlertParamsSchema, - EsQueryAlertState, -} from './alert_type_params'; + EsQueryRuleParams, + EsQueryRuleParamsExtractedParams, + EsQueryRuleParamsSchema, + EsQueryRuleState, +} from './rule_type_params'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { ExecutorOptions } from './types'; import { ActionGroupId, ES_QUERY_ID } from './constants'; import { executor } from './executor'; -import { isEsQueryAlert } from './util'; +import { isEsQueryRule } from './util'; -export function getAlertType( +export function getRuleType( logger: Logger, core: CoreSetup ): RuleType< - EsQueryAlertParams, - EsQueryAlertParamsExtractedParams, - EsQueryAlertState, + EsQueryRuleParams, + EsQueryRuleParamsExtractedParams, + EsQueryRuleState, {}, ActionContext, typeof ActionGroupId > { - const alertTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', { + const ruleTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', { defaultMessage: 'Elasticsearch query', }); @@ -137,11 +137,11 @@ export function getAlertType( return { id: ES_QUERY_ID, - name: alertTypeName, + name: ruleTypeName, actionGroups: [{ id: ActionGroupId, name: actionGroupName }], defaultActionGroupId: ActionGroupId, validate: { - params: EsQueryAlertParamsSchema, + params: EsQueryRuleParamsSchema, }, actionVariables: { context: [ @@ -164,15 +164,15 @@ export function getAlertType( }, useSavedObjectReferences: { extractReferences: (params) => { - if (isEsQueryAlert(params.searchType)) { - return { params: params as EsQueryAlertParamsExtractedParams, references: [] }; + if (isEsQueryRule(params.searchType)) { + return { params: params as EsQueryRuleParamsExtractedParams, references: [] }; } const [searchConfiguration, references] = extractReferences(params.searchConfiguration); - const newParams = { ...params, searchConfiguration } as EsQueryAlertParamsExtractedParams; + const newParams = { ...params, searchConfiguration } as EsQueryRuleParamsExtractedParams; return { params: newParams, references }; }, injectReferences: (params, references) => { - if (isEsQueryAlert(params.searchType)) { + if (isEsQueryRule(params.searchType)) { return params; } return { @@ -183,9 +183,10 @@ export function getAlertType( }, minimumLicenseRequired: 'basic', isExportable: true, - executor: async (options: ExecutorOptions) => { + executor: async (options: ExecutorOptions) => { return await executor(logger, core, options); }, producer: STACK_ALERTS_FEATURE_ID, + doesSetRecoveryContext: true, }; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.test.ts similarity index 96% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.test.ts index a1155fedb7a02..865cf330b1c43 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.test.ts @@ -9,12 +9,12 @@ import { TypeOf } from '@kbn/config-schema'; import type { Writable } from '@kbn/utility-types'; import { Comparator } from '../../../common/comparator_types'; import { - EsQueryAlertParamsSchema, - EsQueryAlertParams, + EsQueryRuleParamsSchema, + EsQueryRuleParams, ES_QUERY_MAX_HITS_PER_EXECUTION, -} from './alert_type_params'; +} from './rule_type_params'; -const DefaultParams: Writable> = { +const DefaultParams: Writable> = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -220,7 +220,7 @@ describe('alertType Params validate()', () => { return () => validate(); } - function validate(): TypeOf { - return EsQueryAlertParamsSchema.validate(params); + function validate(): TypeOf { + return EsQueryRuleParamsSchema.validate(params); } }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.ts similarity index 87% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.ts index d32fce9debbc2..a705e84ae54c7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/rule_type_params.ts @@ -16,19 +16,19 @@ import { getComparatorSchemaType } from '../lib/comparator'; export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000; -// alert type parameters -export type EsQueryAlertParams = TypeOf; -export interface EsQueryAlertState extends RuleTypeState { +// rule type parameters +export type EsQueryRuleParams = TypeOf; +export interface EsQueryRuleState extends RuleTypeState { latestTimestamp: string | undefined; } -export type EsQueryAlertParamsExtractedParams = Omit & { +export type EsQueryRuleParamsExtractedParams = Omit & { searchConfiguration: SerializedSearchSourceFields & { indexRefName: string; }; }; -const EsQueryAlertParamsSchemaProperties = { +const EsQueryRuleParamsSchemaProperties = { size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), timeWindowSize: schema.number({ min: 1 }), timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), @@ -37,14 +37,14 @@ const EsQueryAlertParamsSchemaProperties = { searchType: schema.oneOf([schema.literal('searchSource'), schema.literal('esQuery')], { defaultValue: 'esQuery', }), - // searchSource alert param only + // searchSource rule param only searchConfiguration: schema.conditional( schema.siblingRef('searchType'), schema.literal('searchSource'), schema.object({}, { unknowns: 'allow' }), schema.never() ), - // esQuery alert params only + // esQuery rule params only esQuery: schema.conditional( schema.siblingRef('searchType'), schema.literal('esQuery'), @@ -65,7 +65,7 @@ const EsQueryAlertParamsSchemaProperties = { ), }; -export const EsQueryAlertParamsSchema = schema.object(EsQueryAlertParamsSchemaProperties, { +export const EsQueryRuleParamsSchema = schema.object(EsQueryRuleParamsSchemaProperties, { validate: validateParams, }); @@ -73,7 +73,7 @@ const betweenComparators = new Set(['between', 'notBetween']); // using direct type not allowed, circular reference, so body is typed to any function validateParams(anyParams: unknown): string | undefined { - const { esQuery, thresholdComparator, threshold, searchType } = anyParams as EsQueryAlertParams; + const { esQuery, thresholdComparator, threshold, searchType } = anyParams as EsQueryRuleParams; if (betweenComparators.has(thresholdComparator) && threshold.length === 1) { return i18n.translate('xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage', { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts index 8595870a84940..2b0f0f7a7407c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts @@ -7,15 +7,15 @@ import { RuleExecutorOptions, RuleTypeParams } from '../../types'; import { ActionContext } from './action_context'; -import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; +import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params'; import { ActionGroupId } from './constants'; -export type OnlyEsQueryAlertParams = Omit & { +export type OnlyEsQueryRuleParams = Omit & { searchType: 'esQuery'; }; -export type OnlySearchSourceAlertParams = Omit< - EsQueryAlertParams, +export type OnlySearchSourceRuleParams = Omit< + EsQueryRuleParams, 'esQuery' | 'index' | 'timeField' > & { searchType: 'searchSource'; @@ -23,7 +23,7 @@ export type OnlySearchSourceAlertParams = Omit< export type ExecutorOptions

= RuleExecutorOptions< P, - EsQueryAlertState, + EsQueryRuleState, {}, ActionContext, typeof ActionGroupId diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts index b58a362cd27e9..064a7f64b4c32 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/util.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { EsQueryAlertParams } from './alert_type_params'; +import { EsQueryRuleParams } from './rule_type_params'; -export function isEsQueryAlert(searchType: EsQueryAlertParams['searchType']) { +export function isEsQueryRule(searchType: EsQueryRuleParams['searchType']) { return searchType !== 'searchSource'; } diff --git a/x-pack/plugins/synthetics/common/constants/ui.ts b/x-pack/plugins/synthetics/common/constants/ui.ts index 994cc20536723..226eda1986886 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 2b343cfa68883..3351d3da6140c 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 50497c4c9214c..95ec1c5a62975 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 252b650cc7058..f345b79ae5488 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 81585a9f26a99..d8d645451c18a 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 7c20fcfe1c143..0000000000000 --- 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 30d23128d1e82..e13e982203e1a 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 0000000000000..aaf94f46e283a --- /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 0000000000000..be6e80e3f8469 --- /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 0000000000000..a0e024ef748f8 --- /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 0000000000000..dbece4ae95983 --- /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 0000000000000..61e32d41af6df --- /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 0000000000000..fbaf5c1d536cf --- /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 0000000000000..7bf951b671c95 --- /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 0000000000000..4f285dcb911d1 --- /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 0000000000000..0a9d253d287fc --- /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 0000000000000..9d92c584592d3 --- /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 0000000000000..ece118812c0fb --- /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 0000000000000..fd910f512caa4 --- /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 0000000000000..e98ca2a466f0d --- /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 0000000000000..f692e84354666 --- /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 0000000000000..fc9b014b18481 --- /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 0000000000000..b50d97fcecefa --- /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 0000000000000..8dbeaa74d618d --- /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 2d4412b71f230..ee048593e5de1 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 0000000000000..ad7220e328f3b --- /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 0000000000000..643764f0d3f97 --- /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 0000000000000..41d5c611e97e4 --- /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 3f2150169e2df..f842518af6ec9 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 0f71c9bafa962..ca14e3751c949 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 0000000000000..134d8c024555e --- /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 d33a0fd3c20cc..9f0ab7d740ec5 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 9e229308a402e..0000000000000 --- 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 af657abd77540..4ddd22a23cbdb 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 c3cde2eaffec5..320c41b7ee1f8 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 0000000000000..74a430240b616 --- /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 0000000000000..67b2b9fc1b76b --- /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 27f2599fbc102..75f0604e0c246 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 5bc9517d6aa11..af377f27387a3 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 0000000000000..b9969cca2afc6 --- /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 777e72069f6f2..b500a3d8d8688 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 924fb8baf1da0..e155250eec19b 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 0000000000000..fbe152f290aa7 --- /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 0000000000000..bfc4272b04a67 --- /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 0000000000000..6d92e75977cf6 --- /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 2493f9eb173d8..0000000000000 --- 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/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts deleted file mode 100644 index 5c7dc8360ec9f..0000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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; - -export const serviceLocationsSelector = (state: SyntheticsAppState) => - state.serviceLocations.locations; 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 572d00ce3892f..0000000000000 --- 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 9a66b4e6b9e74..78d26f231fca1 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 1c8ed190fd80e..e358b185fbeb3 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 0000000000000..794e16d0292c5 --- /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 0000000000000..3435c06f6cf8e --- /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 0000000000000..e72f173af6c86 --- /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 0000000000000..98676baf421a9 --- /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 0000000000000..3ced345c6259e --- /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 0000000000000..c38fadc0952a6 --- /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 0000000000000..4593f241b41f5 --- /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 0000000000000..d3134c60f8fd3 --- /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 0000000000000..62ed85ad17e86 --- /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 0000000000000..fd69d44871637 --- /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 0000000000000..b34402556ed91 --- /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 a045c7a7f7bed..7242160901964 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 0000000000000..47d52a73e6850 --- /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/security_solution/public/detections/components/value_lists_management_modal/index.tsx b/x-pack/plugins/synthetics/public/hooks/use_capabilities.ts similarity index 58% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/index.tsx rename to x-pack/plugins/synthetics/public/hooks/use_capabilities.ts index 3571a67040148..5cde14df84f0e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/index.tsx +++ b/x-pack/plugins/synthetics/public/hooks/use_capabilities.ts @@ -5,4 +5,8 @@ * 2.0. */ -export { ValueListsModal } from './modal'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +export const useCanEditSynthetics = () => { + return !!useKibana().services?.application?.capabilities.uptime.save; +}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts index 1d329a598f623..243875cd1de2a 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts @@ -6,6 +6,7 @@ */ import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common'; +import { parseJsonIfString } from '../helpers/parsers'; import { CommonFields, ConfigKey, DataStream } from '../types'; import { DEFAULT_COMMON_FIELDS, @@ -27,7 +28,8 @@ export type CommonNormalizerMap = Record value ? value.slice(0, value.length - 1) : null; -export const jsonToJavascriptNormalizer = (value: string) => (value ? JSON.parse(value) : null); +export const jsonToJavascriptNormalizer = (value: string) => + value ? parseJsonIfString(value) : null; export function getNormalizer(key: string, defaultValues: Fields): Normalizer { return (fields: NewPackagePolicyInput['vars']) => diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_factory_picker/index.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/helpers/parsers.ts similarity index 64% rename from x-pack/plugins/ui_actions_enhanced/public/components/action_factory_picker/index.ts rename to x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/helpers/parsers.ts index 3d7ffed301859..7dcba327386e6 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_factory_picker/index.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/helpers/parsers.ts @@ -5,4 +5,10 @@ * 2.0. */ -export * from './action_factory_picker'; +export function parseJsonIfString(value: T) { + if (typeof value === 'string') { + return JSON.parse(value); + } + + return value; +} diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/http/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/http/normalizers.ts index a4013a0e8024d..f7e7ad3eeac2e 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/http/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/http/normalizers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { parseJsonIfString } from '../helpers/parsers'; import { HTTPFields, ConfigKey, ContentType, contentTypesToMode } from '../types'; import { Normalizer, @@ -56,7 +57,7 @@ export const httpNormalizers: HTTPNormalizerMap = { const requestHeaders = fields?.[ConfigKey.REQUEST_HEADERS_CHECK]?.value; if (requestBody) { const headers = requestHeaders - ? JSON.parse(fields?.[ConfigKey.REQUEST_HEADERS_CHECK]?.value) + ? parseJsonIfString(fields?.[ConfigKey.REQUEST_HEADERS_CHECK]?.value) : defaultHTTPAdvancedFields[ConfigKey.REQUEST_HEADERS_CHECK]; const requestBodyValue = requestBody !== null && requestBody !== undefined diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/tls/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/tls/normalizers.ts index a4cf9f280e9a7..eb52494d2cb96 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/tls/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/tls/normalizers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { parseJsonIfString } from '../helpers/parsers'; import { TLSFields, ConfigKey } from '../types'; import { Normalizer } from '../common/normalizers'; import { defaultTLSFields } from '../contexts'; @@ -41,4 +42,4 @@ export const tlsNormalizers: TLSNormalizerMap = { export const tlsStringToObjectNormalizer = (value: string = '', key: keyof TLSFields) => value ?? defaultTLSFields[key]; export const tlsJsonToObjectNormalizer = (value: string = '', key: keyof TLSFields) => - value ? JSON.parse(value) : defaultTLSFields[key]; + value ? parseJsonIfString(value) : defaultTLSFields[key]; 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 5672c96314dc9..af09f6965083b 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 727f4f6dee72b..6db399175aaa6 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 127655f95e8a6..826beb2cbfe68 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/plugins/synthetics/server/legacy_uptime/lib/telemetry/constants.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/constants.ts index b882ee5f0bea5..ebb462301dea5 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/constants.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/constants.ts @@ -7,6 +7,6 @@ export const MONITOR_UPDATE_CHANNEL = 'synthetics-monitor-update'; export const MONITOR_CURRENT_CHANNEL = 'synthetics-monitor-current'; -export const MONITOR_ERROR_EVENT_CHANNEL = 'synthetics-monitor-error-event'; +export const MONITOR_ERROR_EVENTS_CHANNEL = 'synthetics-monitor-error-events'; export const MONITOR_SYNC_STATE_CHANNEL = 'synthetics-monitor-sync-state'; export const MONITOR_SYNC_EVENTS_CHANNEL = 'synthetics-monitor-sync-events'; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/types.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/types.ts index 3378ec8682969..4680b844b5ae9 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/types.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/telemetry/types.ts @@ -5,7 +5,7 @@ * 2.0. */ import { ServiceLocationErrors } from '../../../../common/runtime_types/monitor_management'; -import { MONITOR_ERROR_EVENT_CHANNEL } from './constants'; +import { MONITOR_ERROR_EVENTS_CHANNEL } from './constants'; export interface MonitorSyncEvent { total: number; @@ -47,7 +47,7 @@ export interface MonitorUpdateTelemetryChannelEvents { // channel name => event type 'synthetics-monitor-update': MonitorUpdateEvent; 'synthetics-monitor-current': MonitorUpdateEvent; - [MONITOR_ERROR_EVENT_CHANNEL]: MonitorErrorEvent; + [MONITOR_ERROR_EVENTS_CHANNEL]: MonitorErrorEvent; 'synthetics-monitor-sync-state': MonitorSyncEvent; 'synthetics-monitor-sync-events': MonitorSyncEvent; } diff --git a/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts b/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts index c1fa607ad0e43..5d0d29a4c8ac5 100644 --- a/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts +++ b/x-pack/plugins/synthetics/server/routes/telemetry/monitor_upgrade_sender.ts @@ -21,7 +21,7 @@ import { TelemetryEventsSender } from '../../legacy_uptime/lib/telemetry/sender' import { MONITOR_UPDATE_CHANNEL, MONITOR_CURRENT_CHANNEL, - MONITOR_ERROR_EVENT_CHANNEL, + MONITOR_ERROR_EVENTS_CHANNEL, MONITOR_SYNC_STATE_CHANNEL, MONITOR_SYNC_EVENTS_CHANNEL, } from '../../legacy_uptime/lib/telemetry/constants'; @@ -77,7 +77,7 @@ export function sendErrorTelemetryEvents( } try { - eventsTelemetry.queueTelemetryEvents(MONITOR_ERROR_EVENT_CHANNEL, [updateEvent]); + eventsTelemetry.queueTelemetryEvents(MONITOR_ERROR_EVENTS_CHANNEL, [updateEvent]); } catch (exc) { logger.error(`queuing telemetry events failed ${exc}`); } diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index a62afcd0a4b3f..b8928fabb3435 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -7000,6 +7000,9 @@ }, "visual_reporting_soft_disabled_error": { "type": "long" + }, + "invalid_layout_parameters_error": { + "type": "long" } } } @@ -7118,6 +7121,9 @@ }, "visual_reporting_soft_disabled_error": { "type": "long" + }, + "invalid_layout_parameters_error": { + "type": "long" } } } @@ -7268,6 +7274,9 @@ }, "visual_reporting_soft_disabled_error": { "type": "long" + }, + "invalid_layout_parameters_error": { + "type": "long" } } } @@ -7418,6 +7427,9 @@ }, "visual_reporting_soft_disabled_error": { "type": "long" + }, + "invalid_layout_parameters_error": { + "type": "long" } } } @@ -8275,6 +8287,9 @@ }, "visual_reporting_soft_disabled_error": { "type": "long" + }, + "invalid_layout_parameters_error": { + "type": "long" } } } @@ -8393,6 +8408,9 @@ }, "visual_reporting_soft_disabled_error": { "type": "long" + }, + "invalid_layout_parameters_error": { + "type": "long" } } } @@ -8543,6 +8561,9 @@ }, "visual_reporting_soft_disabled_error": { "type": "long" + }, + "invalid_layout_parameters_error": { + "type": "long" } } } @@ -8693,6 +8714,9 @@ }, "visual_reporting_soft_disabled_error": { "type": "long" + }, + "invalid_layout_parameters_error": { + "type": "long" } } } diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts index e6391e5860aff..d14b346290a15 100644 --- a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts @@ -7,12 +7,9 @@ import type { IFieldSubType } from '@kbn/es-query'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { - IEsSearchRequest, - IEsSearchResponse, - FieldSpec, - RuntimeField, -} from '@kbn/data-plugin/common'; +import type { IEsSearchRequest, IEsSearchResponse, FieldSpec } from '@kbn/data-plugin/common'; +import type { RuntimeField } from '@kbn/data-views-plugin/common'; + import type { DocValueFields, Maybe } from '../common'; export type BeatFieldsFactoryQueryType = 'beatFields'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx index 4ac9aec83a5cb..447b282356c44 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx @@ -14,7 +14,7 @@ import { REMOVE_COLUMN } from './column_headers/translations'; import { Direction } from '../../../../common/search_strategy'; import { useMountAppended } from '../../utils/use_mount_appended'; import { defaultHeaders, mockBrowserFields, mockTimelineData, TestProviders } from '../../../mock'; -import { ColumnHeaderOptions, TimelineTabs } from '../../../../common/types/timeline'; +import { TimelineTabs } from '../../../../common/types/timeline'; import { TestCellRenderer } from '../../../mock/cell_renderer'; import { mockGlobalState } from '../../../mock/global_state'; import { EuiDataGridColumn } from '@elastic/eui'; @@ -335,26 +335,4 @@ describe('Body', () => { type: 'x-pack/timelines/t-grid/UPDATE_COLUMN_WIDTH', }); }); - - test('it dispatches the `REMOVE_COLUMN` action when there is a field removed from the custom fields', async () => { - const customFieldId = 'my.custom.runtimeField'; - const extraFieldProps = { - ...props, - columnHeaders: [ - ...defaultHeaders, - { id: customFieldId, category: 'my' } as ColumnHeaderOptions, - ], - }; - render( - - - - ); - - expect(mockDispatch).toBeCalledTimes(1); - expect(mockDispatch).toBeCalledWith({ - payload: { columnId: customFieldId, id: 'timeline-test' }, - type: 'x-pack/timelines/t-grid/REMOVE_COLUMN', - }); - }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 955b39179fb13..c2ea57aba3da0 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -18,7 +18,7 @@ import { EuiFlexItem, EuiProgress, } from '@elastic/eui'; -import { getOr, isEmpty } from 'lodash/fp'; +import { getOr } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import React, { ComponentType, @@ -397,20 +397,6 @@ export const BodyComponent = React.memo( } }, [isSelectAllChecked, onSelectPage, selectAll]); - // Clean any removed custom field that may still be present in stored columnHeaders - useEffect(() => { - if (!isEmpty(browserFields) && !isEmpty(columnHeaders)) { - columnHeaders.forEach(({ id: columnId }) => { - if (browserFields.base?.fields?.[columnId] == null) { - const [category] = columnId.split('.'); - if (browserFields[category]?.fields?.[columnId] == null) { - dispatch(tGridActions.removeColumn({ id, columnId })); - } - } - }); - } - }, [browserFields, columnHeaders, dispatch, id]); - const onAlertStatusActionSuccess = useMemo(() => { if (bulkActions && bulkActions !== true) { return bulkActions.onAlertStatusActionSuccess; diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index c654596937387..99f060259497a 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -29,7 +29,7 @@ import { PivotQuery, } from './request'; import { LatestFunctionConfigUI } from '../../../common/types/transform'; -import { RuntimeField } from '@kbn/data-plugin/common'; +import type { RuntimeField } from '@kbn/data-views-plugin/common'; const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index e56731b512ab3..e8c5ea1626a9a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -27,7 +27,7 @@ jest.mock('./use_api'); import { useAppDependencies } from '../__mocks__/app_dependencies'; import { MlSharedContext } from '../__mocks__/shared_context'; -import { RuntimeField } from '@kbn/data-plugin/common'; +import type { RuntimeField } from '@kbn/data-views-plugin/common'; const query: SimpleQuery = { query_string: { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 975eacb569b67..649683182dcab 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -29,7 +29,7 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/public'; import { DuplicateDataViewError } from '@kbn/data-plugin/public'; -import type { RuntimeField } from '@kbn/data-plugin/common'; +import type { RuntimeField } from '@kbn/data-views-plugin/common'; import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms'; import { isGetTransformsStatsResponseSchema, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts index ea0c5b8c471bd..134e22b714bad 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts @@ -8,7 +8,7 @@ import { getPivotDropdownOptions } from '.'; import { DataView } from '@kbn/data-views-plugin/public'; import { FilterAggForm } from './filter_agg/components'; -import type { RuntimeField } from '@kbn/data-plugin/common'; +import type { RuntimeField } from '@kbn/data-views-plugin/common'; describe('Transform: Define Pivot Common', () => { test('getPivotDropdownOptions()', () => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx index a2305b88fbdda..16febe74114b0 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx @@ -10,7 +10,8 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import { FilterAggForm } from './filter_agg_form'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; -import { KBN_FIELD_TYPES, RuntimeField } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import type { RuntimeField } from '@kbn/data-views-plugin/common'; import { DataView } from '@kbn/data-views-plugin/public'; import { FilterTermForm } from './filter_term_form'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 21fd2e7ecd302..44bde053ebfb5 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2711,7 +2711,6 @@ "dataViews.deprecations.scriptedFields.manualStepTwoMessage": "Mettez à jour les vues de données {numberOfIndexPatternsWithScriptedFields} qui ont des champs scriptés pour qu’elles utilisent des champs d’exécution. Dans la plupart des cas, pour migrer des scripts existants, vous devrez remplacer \"return ;\" par \"emit();\". Vues de données avec au moins un champ scripté : {allTitles}", "dataViews.deprecations.scriptedFieldsMessage": "Vous avez {numberOfIndexPatternsWithScriptedFields} vues de données ({titlesPreview}...) qui utilisent des champs scriptés. Les champs scriptés sont déclassés et seront supprimés à l’avenir. Utilisez plutôt des champs d’exécution.", "dataViews.deprecations.scriptedFieldsTitle": "Vues de données utilisant des champs scriptés trouvées", - "dataViews.ensureDefaultIndexPattern.bannerLabel": "Pour visualiser et explorer des données dans Kibana, vous devez créer un modèle d'indexation afin d’extraire les données d'Elasticsearch.", "dataViews.fetchFieldErrorTitle": "Erreur lors de l’extraction des champs pour la vue de données {title} (ID : {id})", "dataViews.functions.dataViewLoad.help": "Charge une vue de données", "dataViews.functions.dataViewLoad.id.help": "ID de vue de données à charger", @@ -2932,7 +2931,6 @@ "discover.fieldNameIcons.unknownFieldAriaLabel": "Champ inconnu", "discover.fieldNameIcons.versionFieldAriaLabel": "Champ de version", "discover.goToDiscoverMainViewButtonText": "Accédez à la vue principale de Discover", - "discover.grid.copyToClipBoardButton": "Copier dans le presse-papiers", "discover.grid.documentHeader": "Document", "discover.grid.filterFor": "Filtrer sur", "discover.grid.filterForAria": "Filtrer sur cette {value}", @@ -5051,7 +5049,6 @@ "kibana-react.tableListView.listing.listingLimitExceededTitle": "Limite de listing dépassée", "kibana-react.tableListView.listing.table.actionTitle": "Actions", "kibana-react.tableListView.listing.table.editActionDescription": "Modifier", - "kibana-react.tableListView.listing.table.editActionName": "Modifier", "kibana-react.tableListView.listing.unableToDeleteDangerMessage": "Impossible de supprimer la/le/les {entityName}(s)", "kibanaOverview.addData.sampleDataButtonLabel": "Essayer l’exemple de données", "kibanaOverview.addData.sectionTitle": "Ingérer des données", @@ -5581,6 +5578,90 @@ "uiActions.errors.incompatibleAction": "Action non compatible", "uiActions.triggers.rowClickkDescription": "Un clic sur une ligne de tableau", "uiActions.triggers.rowClickTitle": "Clic sur ligne de tableau", + "uiActionsEnhanced.components.actionWizard.betaActionLabel": "Bêta", + "uiActionsEnhanced.components.actionWizard.betaActionTooltip": "Cette action est en version bêta et susceptible d'être modifiée. La conception et le code sont moins matures que les fonctionnalités officielles en disponibilité générale et sont fournis tels quels sans aucune garantie. Les fonctionnalités bêta ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale. Nous vous remercions de bien vouloir nous aider en nous signalant les bugs ou en nous envoyant d'autres commentaires.", + "uiActionsEnhanced.components.actionWizard.changeButton": "Modifier", + "uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "Niveau de licence insuffisant", + "uiActionsEnhanced.components.actionWizard.triggerPickerHelpText": "Qu'est-ce que c'est ?", + "uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip": "Détermine quand la recherche s'affiche dans le menu contextuel", + "uiActionsEnhanced.components.actionWizard.triggerPickerLabel": "Afficher l'option sur :", + "uiActionsEnhanced.components.DrilldownForm.betaActionLabel": "Bêta", + "uiActionsEnhanced.components.DrilldownForm.betaActionTooltip": "Cette action est en version bêta et susceptible d'être modifiée. La conception et le code sont moins matures que les fonctionnalités officielles en disponibilité générale et sont fournis tels quels sans aucune garantie. Les fonctionnalités bêta ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale. Nous vous remercions de bien vouloir nous aider en nous signalant les bugs ou en nous envoyant d'autres commentaires.", + "uiActionsEnhanced.components.DrilldownForm.changeButton": "Modifier", + "uiActionsEnhanced.components.DrilldownForm.drilldownAction": "Action", + "uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel": "Obtenir plus d'actions", + "uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown": "Nom", + "uiActionsEnhanced.components.DrilldownForm.trigger": "Déclencher", + "uiActionsEnhanced.components.DrilldownForm.untitledDrilldown": "Recherche sans titre", + "uiActionsEnhanced.components.DrilldownTable.actionColumnTitle": "Action", + "uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel": "Copier", + "uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel": "Créer nouvelle", + "uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel": "Supprimer ({count})", + "uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel": "Modifier", + "uiActionsEnhanced.components.DrilldownTable.nameColumnTitle": "Nom", + "uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel": "Sélectionner cette recherche", + "uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle": "Déclencher", + "uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle": "Action", + "uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel": "Copier ({count})", + "uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle": "Nom", + "uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage": "Sélectionner ce modèle", + "uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction": "Copier", + "uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle": "Panneau", + "uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle": "Déclencher", + "uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "Ce type de déclenchement n'est pas pris en charge par ce panneau", + "uiActionsEnhanced.components.TriggerPickerItem.unknown": "Inconnu", + "uiActionsEnhanced.CustomActions": "Actions personnalisées", + "uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "Ajouter au panneau", + "uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "Annuler", + "uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "Plage temporelle", + "uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "Retirer", + "uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "Mettre à jour", + "uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "Personnaliser la plage temporelle du panneau", + "uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "Personnaliser la plage temporelle", + "uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "Copier la recherche existante", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "Les recherches vous permettent de définir de nouveaux comportements pour l'interaction avec les panneaux. Vous pouvez ajouter plusieurs actions et remplacer le filtre par défaut.", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "Masquer", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "Afficher les documents", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle": "Créer une recherche", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel": "Supprimer une recherche", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle": "Modifier une recherche", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError": "Niveau de licence insuffisant", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType": "Le type de recherche {type} n'existe pas", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText": "Enregistrez votre tableau de bord avant de tester.", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle": "Recherche \"{drilldownName}\" créée", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText": "Enregistrez votre tableau de bord avant de tester.", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle": "Recherche supprimée", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText": "Enregistrez votre tableau de bord avant de tester.", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle": "Recherche \"{drilldownName}\" mise à jour", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle": "Erreur lors de l'enregistrement de la recherche", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText": "Enregistrez votre tableau de bord avant de tester.", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle": "{n} recherches supprimées", + "uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel": "Précédent", + "uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel": "Fermer", + "uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton": "Créer une recherche", + "uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title": "Créer une recherche", + "uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body": "{count, number} {count, plural, one {recherche} other {recherches}} copiée(s).", + "uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss": "Rejeter", + "uiActionsEnhanced.drilldowns.containers.DrilldownManager.createNew": "Créer nouvelle", + "uiActionsEnhanced.drilldowns.containers.DrilldownManager.manage": "Gérer", + "uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton": "Enregistrer", + "uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title": "Modifier une recherche", + "uiActionsEnhanced.drilldowns.drilldownManager.state.defaultTitle": "Recherches", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions": "Options supplémentaires", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "Ajouter une variable", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription": "Si elle est activée, l'URL sera précédée de l’encodage-pourcent comme caractère d'échappement", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl": "Encoder l'URL", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel": "Ouvrir dans une nouvelle fenêtre", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText": "Veuillez noter que dans l'aperçu, les variables \\{\\{event.*\\}\\} sont remplacées par des valeurs factices.", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel": "Aperçu de l'URL :", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText": "Aperçu", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel": "Entrer l'URL", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText": "Exemple : {exampleUrl}", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "Aide pour la syntaxe", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "Variables de filtre", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "Aide", + "uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "Format non valide : {message}", + "uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "Format non valide. Exemple : {exampleUrl}", "utils.filename.pathWarning": "Le chemin est peut-être incorrectement formé ; vérifiez la valeur", "utils.filename.wildcardWarning": "L’utilisation de caractères génériques dans les chemins de fichier peut affecter les performances du point de terminaison", "visDefaultEditor.advancedToggle.advancedLinkLabel": "Avancé", @@ -12223,8 +12304,6 @@ "xpack.fleet.agentBulkActions.selectAll": "Tout sélectionner sur toutes les pages", "xpack.fleet.agentBulkActions.totalAgents": "Affichage {count, plural, one {d'# agent} other {de # agents}}", "xpack.fleet.agentBulkActions.totalAgentsWithLimit": "Affichage de {count} agent(s) sur {total}", - "xpack.fleet.agentBulkActions.unenrollAgents": "Désinscrire les agents", - "xpack.fleet.agentBulkActions.upgradeAgents": "Mettre à niveau les agents", "xpack.fleet.agentDetails.actionsButton": "Actions", "xpack.fleet.agentDetails.agentDetailsTitle": "Agent \"{id}\"", "xpack.fleet.agentDetails.agentNotFoundErrorDescription": "Impossible de trouver l'ID d'agent {agentId}", @@ -12247,9 +12326,7 @@ "xpack.fleet.agentDetails.versionLabel": "Version d'agent", "xpack.fleet.agentDetails.viewAgentListTitle": "Afficher tous les agents", "xpack.fleet.agentDetails.viewDashboardButtonLabel": "Afficher le tableau de bord de l'agent", - "xpack.fleet.agentDetailsIntegrations.actionsLabel": "Actions", "xpack.fleet.agentDetailsIntegrations.inputTypeEndpointText": "Point de terminaison", - "xpack.fleet.agentDetailsIntegrations.inputTypeLabel": "Entrée", "xpack.fleet.agentDetailsIntegrations.inputTypeLogText": "Logs", "xpack.fleet.agentDetailsIntegrations.inputTypeMetricsText": "Indicateurs", "xpack.fleet.agentDetailsIntegrations.viewLogsButton": "Afficher les logs", @@ -25864,18 +25941,14 @@ "xpack.securitySolution.lastEventTime.failSearchDescription": "Impossible de lancer une recherche sur la dernière heure de l'événement", "xpack.securitySolution.licensing.unsupportedMachineLearningMessage": "Votre licence ne prend pas en charge le Machine Learning. Veuillez mettre votre licence à niveau.", "xpack.securitySolution.list.backButton": "Retour", - "xpack.securitySolution.lists.cancelValueListsUploadTitle": "Annuler le chargement", "xpack.securitySolution.lists.closeValueListsModalTitle": "Fermer", - "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButton": "Charger les listes de valeurs", "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButtonTooltip": "Utiliser les listes de valeurs pour créer une exception lorsqu'une valeur de champ correspond à une valeur trouvée dans une liste", "xpack.securitySolution.lists.referenceModalCancelButton": "Annuler", "xpack.securitySolution.lists.referenceModalDeleteButton": "Retirer la liste de valeurs", "xpack.securitySolution.lists.referenceModalDescription": "Cette liste de valeurs est associée à ({referenceCount}) {referenceCount, plural, =1 {liste} other {listes}} d'exception. Le retrait de cette liste supprimera tous les éléments d'exception qui référencent cette liste de valeurs.", "xpack.securitySolution.lists.referenceModalTitle": "Retirer la liste de valeurs", - "xpack.securitySolution.lists.uploadValueListDescription": "Charger les listes de valeurs uniques à utiliser lors de l'écriture d'exceptions aux règles.", "xpack.securitySolution.lists.uploadValueListExtensionValidationMessage": "Le fichier doit être de l'un des types suivants : [{fileTypes}]", "xpack.securitySolution.lists.uploadValueListPrompt": "Sélectionner ou glisser-déposer un fichier", - "xpack.securitySolution.lists.uploadValueListTitle": "Charger les listes de valeurs", "xpack.securitySolution.lists.valueListsExportError": "Une erreur s'est produite lors de l'exportation de la liste de valeurs.", "xpack.securitySolution.lists.valueListsForm.ipRadioLabel": "Adresses IP", "xpack.securitySolution.lists.valueListsForm.ipRangesRadioLabel": "Plages IP", @@ -25891,11 +25964,7 @@ "xpack.securitySolution.lists.valueListsTable.fileNameColumn": "Nom de fichier", "xpack.securitySolution.lists.valueListsTable.title": "Listes de valeurs", "xpack.securitySolution.lists.valueListsTable.typeColumn": "Type", - "xpack.securitySolution.lists.valueListsTable.uploadDateColumn": "Charger la date", - "xpack.securitySolution.lists.valueListsUploadButton": "Charger la liste", "xpack.securitySolution.lists.valueListsUploadError": "Une erreur s'est produite lors du chargement de la liste de valeurs.", - "xpack.securitySolution.lists.valueListsUploadSuccess": "La liste de valeurs \"{fileName}\" a été chargée", - "xpack.securitySolution.lists.valueListsUploadSuccessTitle": "Liste de valeurs chargée", "xpack.securitySolution.management.policiesSelector.globalEntries": "Entrées globales", "xpack.securitySolution.management.policiesSelector.label": "Politiques", "xpack.securitySolution.management.policiesSelector.unassignedEntries": "Entrées non affectées", @@ -27759,8 +27828,6 @@ "xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel": "Tableau de valeurs à utiliser comme seuil ; \"between\" et \"notBetween\" requièrent deux valeurs, les autres n'en requièrent qu'une seule.", "xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "Titre pour l'alerte.", "xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "Valeur ayant rempli la condition de seuil.", - "xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "Le nombre de documents correspondants est {thresholdComparator} {threshold}", - "xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "l'alerte \"{name}\" correspond à la recherche", "xpack.stackAlerts.esQuery.alertTypeTitle": "Recherche Elasticsearch", "xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "thresholdComparator spécifié non valide : {comparator}", "xpack.stackAlerts.esQuery.invalidEsQueryErrorMessage": "[esQuery] : doit être au format JSON valide", @@ -29158,90 +29225,6 @@ "xpack.triggersActionsUI.timeUnits.secondLabel": "{timeValue, plural, one {seconde} other {secondes}}", "xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage": "Le type d'objet \"{id}\" n'est pas enregistré.", "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "Le type d'objet \"{id}\" est déjà enregistré.", - "xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel": "Bêta", - "xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip": "Cette action est en version bêta et susceptible d'être modifiée. La conception et le code sont moins matures que les fonctionnalités officielles en disponibilité générale et sont fournis tels quels sans aucune garantie. Les fonctionnalités bêta ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale. Nous vous remercions de bien vouloir nous aider en nous signalant les bugs ou en nous envoyant d'autres commentaires.", - "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "Modifier", - "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "Niveau de licence insuffisant", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText": "Qu'est-ce que c'est ?", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip": "Détermine quand la recherche s'affiche dans le menu contextuel", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel": "Afficher l'option sur :", - "xpack.uiActionsEnhanced.components.DrilldownForm.betaActionLabel": "Bêta", - "xpack.uiActionsEnhanced.components.DrilldownForm.betaActionTooltip": "Cette action est en version bêta et susceptible d'être modifiée. La conception et le code sont moins matures que les fonctionnalités officielles en disponibilité générale et sont fournis tels quels sans aucune garantie. Les fonctionnalités bêta ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale. Nous vous remercions de bien vouloir nous aider en nous signalant les bugs ou en nous envoyant d'autres commentaires.", - "xpack.uiActionsEnhanced.components.DrilldownForm.changeButton": "Modifier", - "xpack.uiActionsEnhanced.components.DrilldownForm.drilldownAction": "Action", - "xpack.uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel": "Obtenir plus d'actions", - "xpack.uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown": "Nom", - "xpack.uiActionsEnhanced.components.DrilldownForm.trigger": "Déclencher", - "xpack.uiActionsEnhanced.components.DrilldownForm.untitledDrilldown": "Recherche sans titre", - "xpack.uiActionsEnhanced.components.DrilldownTable.actionColumnTitle": "Action", - "xpack.uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel": "Copier", - "xpack.uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel": "Créer nouvelle", - "xpack.uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel": "Supprimer ({count})", - "xpack.uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel": "Modifier", - "xpack.uiActionsEnhanced.components.DrilldownTable.nameColumnTitle": "Nom", - "xpack.uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel": "Sélectionner cette recherche", - "xpack.uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle": "Déclencher", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle": "Action", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel": "Copier ({count})", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle": "Nom", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage": "Sélectionner ce modèle", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction": "Copier", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle": "Panneau", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle": "Déclencher", - "xpack.uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "Ce type de déclenchement n'est pas pris en charge par ce panneau", - "xpack.uiActionsEnhanced.components.TriggerPickerItem.unknown": "Inconnu", - "xpack.uiActionsEnhanced.CustomActions": "Actions personnalisées", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "Ajouter au panneau", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "Annuler", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "Plage temporelle", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "Retirer", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "Mettre à jour", - "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "Personnaliser la plage temporelle du panneau", - "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "Personnaliser la plage temporelle", - "xpack.uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "Copier la recherche existante", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "Les recherches vous permettent de définir de nouveaux comportements pour l'interaction avec les panneaux. Vous pouvez ajouter plusieurs actions et remplacer le filtre par défaut.", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "Masquer", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "Afficher les documents", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle": "Créer une recherche", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel": "Supprimer une recherche", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle": "Modifier une recherche", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError": "Niveau de licence insuffisant", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType": "Le type de recherche {type} n'existe pas", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText": "Enregistrez votre tableau de bord avant de tester.", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle": "Recherche \"{drilldownName}\" créée", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText": "Enregistrez votre tableau de bord avant de tester.", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle": "Recherche supprimée", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText": "Enregistrez votre tableau de bord avant de tester.", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle": "Recherche \"{drilldownName}\" mise à jour", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle": "Erreur lors de l'enregistrement de la recherche", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText": "Enregistrez votre tableau de bord avant de tester.", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle": "{n} recherches supprimées", - "xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel": "Précédent", - "xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel": "Fermer", - "xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton": "Créer une recherche", - "xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title": "Créer une recherche", - "xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body": "{count, number} {count, plural, one {recherche} other {recherches}} copiée(s).", - "xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss": "Rejeter", - "xpack.uiActionsEnhanced.drilldowns.containers.DrilldownManager.createNew": "Créer nouvelle", - "xpack.uiActionsEnhanced.drilldowns.containers.DrilldownManager.manage": "Gérer", - "xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton": "Enregistrer", - "xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title": "Modifier une recherche", - "xpack.uiActionsEnhanced.drilldowns.drilldownManager.state.defaultTitle": "Recherches", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions": "Options supplémentaires", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "Ajouter une variable", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription": "Si elle est activée, l'URL sera précédée de l’encodage-pourcent comme caractère d'échappement", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl": "Encoder l'URL", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel": "Ouvrir dans une nouvelle fenêtre", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText": "Veuillez noter que dans l'aperçu, les variables \\{\\{event.*\\}\\} sont remplacées par des valeurs factices.", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel": "Aperçu de l'URL :", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText": "Aperçu", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel": "Entrer l'URL", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText": "Exemple : {exampleUrl}", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "Aide pour la syntaxe", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "Variables de filtre", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "Aide", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "Format non valide : {message}", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "Format non valide. Exemple : {exampleUrl}", "xpack.upgradeAssistant.app.deniedPrivilegeDescription": "Afin d'utiliser l'assistant de mise à niveau et de résoudre les problèmes de déclassement, vous devez disposer d’un accès permettant de gérer tous les espaces Kibana.", "xpack.upgradeAssistant.app.deniedPrivilegeTitle": "Rôle d'administrateur Kibana requis", "xpack.upgradeAssistant.appTitle": "Assistant de mise à niveau", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9236e88679d9b..b95cca44c89b8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2804,7 +2804,6 @@ "dataViews.deprecations.scriptedFields.manualStepTwoMessage": "ランタイムフィールドを使用するには、スクリプト化されたフィールドがある{numberOfIndexPatternsWithScriptedFields}インデックスパターンを更新します。ほとんどの場合、既存のスクリプトを移行するには、「return ;」から「emit();」に変更する必要があります。1つ以上のスクリプト化されたフィールドがあるインデックスパターン:{allTitles}", "dataViews.deprecations.scriptedFieldsMessage": "スクリプト化されたフィールドを使用する{numberOfIndexPatternsWithScriptedFields}インデックスパターン({titlesPreview}...)があります。スクリプト化されたフィールドは廃止予定であり、今後は削除されます。ランタイムフィールドを使用してください。", "dataViews.deprecations.scriptedFieldsTitle": "スクリプト化されたフィールドを使用しているインデックスパターンが見つかりました", - "dataViews.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "dataViews.fetchFieldErrorTitle": "データビューのフィールド取得中にエラーが発生 {title}(ID:{id})", "dataViews.functions.dataViewLoad.help": "データビューを読み込みます", "dataViews.functions.dataViewLoad.id.help": "読み込むデータビューID", @@ -3026,7 +3025,6 @@ "discover.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド", "discover.fieldNameIcons.versionFieldAriaLabel": "バージョンフィールド", "discover.goToDiscoverMainViewButtonText": "Discoverメインビューに移動", - "discover.grid.copyToClipBoardButton": "クリップボードにコピー", "discover.grid.documentHeader": "ドキュメント", "discover.grid.filterFor": "フィルター", "discover.grid.filterForAria": "この{value}でフィルターを適用", @@ -5147,7 +5145,6 @@ "kibana-react.tableListView.listing.noMatchedItemsMessage": "検索条件に一致する {entityNamePlural} がありません。", "kibana-react.tableListView.listing.table.actionTitle": "アクション", "kibana-react.tableListView.listing.table.editActionDescription": "編集", - "kibana-react.tableListView.listing.table.editActionName": "編集", "kibana-react.tableListView.listing.unableToDeleteDangerMessage": "{entityName} を削除できません", "kibanaOverview.addData.sampleDataButtonLabel": "サンプルデータを試す", "kibanaOverview.addData.sectionTitle": "データを取り込む", @@ -5682,6 +5679,90 @@ "uiActions.errors.incompatibleAction": "操作に互換性がありません", "uiActions.triggers.rowClickkDescription": "テーブル行をクリック", "uiActions.triggers.rowClickTitle": "テーブル行クリック", + "uiActionsEnhanced.components.actionWizard.betaActionLabel": "ベータ", + "uiActionsEnhanced.components.actionWizard.betaActionTooltip": "このアクションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。バグを報告したり、その他のフィードバックを提供したりして、当社を支援してください。", + "uiActionsEnhanced.components.actionWizard.changeButton": "変更", + "uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "不十分なライセンスレベル", + "uiActionsEnhanced.components.actionWizard.triggerPickerHelpText": "概要", + "uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip": "ドリルダウンがコンテキストメニューに表示されるタイミングを決定します。", + "uiActionsEnhanced.components.actionWizard.triggerPickerLabel": "オプションを表示:", + "uiActionsEnhanced.components.DrilldownForm.betaActionLabel": "ベータ", + "uiActionsEnhanced.components.DrilldownForm.betaActionTooltip": "このアクションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。バグを報告したり、その他のフィードバックを提供したりして、当社を支援してください。", + "uiActionsEnhanced.components.DrilldownForm.changeButton": "変更", + "uiActionsEnhanced.components.DrilldownForm.drilldownAction": "アクション", + "uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel": "さらにアクションを表示", + "uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown": "名前", + "uiActionsEnhanced.components.DrilldownForm.trigger": "トリガー", + "uiActionsEnhanced.components.DrilldownForm.untitledDrilldown": "無題のドリルダウン", + "uiActionsEnhanced.components.DrilldownTable.actionColumnTitle": "アクション", + "uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel": "コピー", + "uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel": "新規作成", + "uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel": "削除({count})", + "uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel": "編集", + "uiActionsEnhanced.components.DrilldownTable.nameColumnTitle": "名前", + "uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel": "このドリルダウンを選択", + "uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle": "トリガー", + "uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle": "アクション", + "uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel": "コピー({count})", + "uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle": "名前", + "uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage": "このテンプレートを選択", + "uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction": "コピー", + "uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle": "パネル", + "uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle": "トリガー", + "uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "このトリガータイプはこのパネルでサポートされていません", + "uiActionsEnhanced.components.TriggerPickerItem.unknown": "不明", + "uiActionsEnhanced.CustomActions": "カスタムアクション", + "uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "パネルに追加", + "uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "キャンセル", + "uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "時間範囲", + "uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "削除", + "uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", + "uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "パネルの時間範囲のカスタマイズ", + "uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "時間範囲のカスタマイズ", + "uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "既存のドリルダウンをコピー", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "ドリルダウンにより、パネルと連携する新しい動作を定義できます。複数のアクションを追加し、デフォルトフィルターを無効化できます。", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "非表示", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "ドキュメントを表示", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle": "ドリルダウンを作成", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel": "ドリルダウンを削除", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle": "ドリルダウンを編集", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError": "不十分なライセンスレベル", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType": "ドリルダウンタイプ{type}が存在しません", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText": "テストする前にダッシュボードを保存してください。", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle": "ドリルダウン「{drilldownName}」が作成されました", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText": "テストする前にダッシュボードを保存してください。", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle": "ドリルダウンが削除されました", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText": "テストする前にダッシュボードを保存してください。", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle": "ドリルダウン「{drilldownName}」が更新されました", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle": "ドリルダウンの保存エラー", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText": "テストする前にダッシュボードを保存してください。", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle": "{n}個のドリルダウンが削除されました", + "uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel": "戻る", + "uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel": "閉じる", + "uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton": "ドリルダウンを作成", + "uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title": "ドリルダウンを作成", + "uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body": "{count, number} {count, plural, other {個のドリルダウン}}がコピーされました。", + "uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss": "閉じる", + "uiActionsEnhanced.drilldowns.containers.DrilldownManager.createNew": "新規作成", + "uiActionsEnhanced.drilldowns.containers.DrilldownManager.manage": "管理", + "uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton": "保存", + "uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title": "ドリルダウンを編集", + "uiActionsEnhanced.drilldowns.drilldownManager.state.defaultTitle": "ドリルダウン", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions": "その他のオプション", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "変数を追加", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription": "有効な場合、URLはパーセントエンコーディングを使用してエスケープされます", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl": "URLのエンコード", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel": "新しいウィンドウで開く", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText": "プレビュー\\{\\{event.*\\}\\}では、変数にダミー値が代入されます。", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel": "URLプレビュー:", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText": "プレビュー", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel": "URLを入力", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText": "例:{exampleUrl}", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "構文ヘルプ", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "変数をフィルター", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "ヘルプ", + "uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "無効な形式:{message}", + "uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "無効なフォーマット。例:{exampleUrl}", "utils.filename.pathWarning": "パスの形式が正しくない可能性があります。値を検証してください", "utils.filename.wildcardWarning": "ファイルパスでワイルドカードを使用すると、エンドポイントのパフォーマンスに影響する可能性があります", "visDefaultEditor.advancedToggle.advancedLinkLabel": "高度な設定", @@ -12322,8 +12403,6 @@ "xpack.fleet.agentBulkActions.selectAll": "すべてのページのすべての項目を選択", "xpack.fleet.agentBulkActions.totalAgents": "{count, plural, other {#個のエージェント}}を表示しています", "xpack.fleet.agentBulkActions.totalAgentsWithLimit": "{count}/{total}個のエージェントを表示しています", - "xpack.fleet.agentBulkActions.unenrollAgents": "エージェントの登録を解除", - "xpack.fleet.agentBulkActions.upgradeAgents": "エージェントをアップグレード", "xpack.fleet.agentDetails.actionsButton": "アクション", "xpack.fleet.agentDetails.agentDetailsTitle": "エージェント'{id}'", "xpack.fleet.agentDetails.agentNotFoundErrorDescription": "エージェントID {agentId}が見つかりません", @@ -12346,9 +12425,7 @@ "xpack.fleet.agentDetails.versionLabel": "エージェントバージョン", "xpack.fleet.agentDetails.viewAgentListTitle": "すべてのエージェントを表示", "xpack.fleet.agentDetails.viewDashboardButtonLabel": "エージェントダッシュボードを表示", - "xpack.fleet.agentDetailsIntegrations.actionsLabel": "アクション", "xpack.fleet.agentDetailsIntegrations.inputTypeEndpointText": "エンドポイント", - "xpack.fleet.agentDetailsIntegrations.inputTypeLabel": "インプット", "xpack.fleet.agentDetailsIntegrations.inputTypeLogText": "ログ", "xpack.fleet.agentDetailsIntegrations.inputTypeMetricsText": "メトリック", "xpack.fleet.agentDetailsIntegrations.viewLogsButton": "ログを表示", @@ -26015,18 +26092,14 @@ "xpack.securitySolution.lastEventTime.failSearchDescription": "前回のイベント時刻で検索を実行できませんでした", "xpack.securitySolution.licensing.unsupportedMachineLearningMessage": "ご使用のライセンスは機械翻訳をサポートしていません。ライセンスをアップグレードしてください。", "xpack.securitySolution.list.backButton": "戻る", - "xpack.securitySolution.lists.cancelValueListsUploadTitle": "アップロードのキャンセル", "xpack.securitySolution.lists.closeValueListsModalTitle": "閉じる", - "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButton": "値リストのアップロード", "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButtonTooltip": "値リストを使用して、フィールド値がリストの値と一致したときに例外を作成します", "xpack.securitySolution.lists.referenceModalCancelButton": "キャンセル", "xpack.securitySolution.lists.referenceModalDeleteButton": "値リストの削除", "xpack.securitySolution.lists.referenceModalDescription": "この値リストは、({referenceCount})例外{referenceCount, plural, other {リスト}}に関連付けられています。このリストを削除すると、この値リストを参照するすべての例外アイテムが削除されます。", "xpack.securitySolution.lists.referenceModalTitle": "値リストの削除", - "xpack.securitySolution.lists.uploadValueListDescription": "ルール例外の書き込み中に使用する単一値リストをアップロードします。", "xpack.securitySolution.lists.uploadValueListExtensionValidationMessage": "ファイルは次の種類のいずれかでなければなりません:[{fileTypes}]", "xpack.securitySolution.lists.uploadValueListPrompt": "ファイルを選択するかドラッグ &amp; ドロップしてください", - "xpack.securitySolution.lists.uploadValueListTitle": "値リストのアップロード", "xpack.securitySolution.lists.valueListsExportError": "値リストのエクスポート中にエラーが発生しました。", "xpack.securitySolution.lists.valueListsForm.ipRadioLabel": "IP アドレス", "xpack.securitySolution.lists.valueListsForm.ipRangesRadioLabel": "IP 範囲", @@ -26042,11 +26115,7 @@ "xpack.securitySolution.lists.valueListsTable.fileNameColumn": "ファイル名", "xpack.securitySolution.lists.valueListsTable.title": "値リスト", "xpack.securitySolution.lists.valueListsTable.typeColumn": "型", - "xpack.securitySolution.lists.valueListsTable.uploadDateColumn": "アップロード日", - "xpack.securitySolution.lists.valueListsUploadButton": "リストのアップロード", "xpack.securitySolution.lists.valueListsUploadError": "値リストのアップロードエラーが発生しました。", - "xpack.securitySolution.lists.valueListsUploadSuccess": "値リスト「{fileName}」はアップロードされませんでした", - "xpack.securitySolution.lists.valueListsUploadSuccessTitle": "値リストがアップロードされました", "xpack.securitySolution.management.policiesSelector.globalEntries": "グローバルエントリ", "xpack.securitySolution.management.policiesSelector.label": "ポリシー", "xpack.securitySolution.management.policiesSelector.unassignedEntries": "割り当てられていないエントリ", @@ -27919,8 +27988,6 @@ "xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel": "しきい値として使用する値の配列。「between」と「notBetween」には2つの値が必要です。その他は1つの値が必要です。", "xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "アラートのタイトル。", "xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "しきい値条件を満たした値。", - "xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "一致するドキュメント数は{thresholdComparator} {threshold}です", - "xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "アラート'{name}'はクエリと一致しました", "xpack.stackAlerts.esQuery.alertTypeTitle": "Elasticsearch クエリ", "xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "無効な thresholdComparator が指定されました:{comparator}", "xpack.stackAlerts.esQuery.invalidEsQueryErrorMessage": "[esQuery]:有効なJSONでなければなりません", @@ -29326,90 +29393,6 @@ "xpack.triggersActionsUI.timeUnits.secondLabel": "{timeValue, plural, other {秒}}", "xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage": "オブジェクトタイプ「{id}」は登録されていません。", "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "オブジェクトタイプ「{id}」はすでに登録されています。", - "xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel": "ベータ", - "xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip": "このアクションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。バグを報告したり、その他のフィードバックを提供したりして、当社を支援してください。", - "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "変更", - "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "不十分なライセンスレベル", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText": "概要", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip": "ドリルダウンがコンテキストメニューに表示されるタイミングを決定します。", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel": "オプションを表示:", - "xpack.uiActionsEnhanced.components.DrilldownForm.betaActionLabel": "ベータ", - "xpack.uiActionsEnhanced.components.DrilldownForm.betaActionTooltip": "このアクションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。バグを報告したり、その他のフィードバックを提供したりして、当社を支援してください。", - "xpack.uiActionsEnhanced.components.DrilldownForm.changeButton": "変更", - "xpack.uiActionsEnhanced.components.DrilldownForm.drilldownAction": "アクション", - "xpack.uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel": "さらにアクションを表示", - "xpack.uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown": "名前", - "xpack.uiActionsEnhanced.components.DrilldownForm.trigger": "トリガー", - "xpack.uiActionsEnhanced.components.DrilldownForm.untitledDrilldown": "無題のドリルダウン", - "xpack.uiActionsEnhanced.components.DrilldownTable.actionColumnTitle": "アクション", - "xpack.uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel": "コピー", - "xpack.uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel": "新規作成", - "xpack.uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel": "削除({count})", - "xpack.uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel": "編集", - "xpack.uiActionsEnhanced.components.DrilldownTable.nameColumnTitle": "名前", - "xpack.uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel": "このドリルダウンを選択", - "xpack.uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle": "トリガー", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle": "アクション", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel": "コピー({count})", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle": "名前", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage": "このテンプレートを選択", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction": "コピー", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle": "パネル", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle": "トリガー", - "xpack.uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "このトリガータイプはこのパネルでサポートされていません", - "xpack.uiActionsEnhanced.components.TriggerPickerItem.unknown": "不明", - "xpack.uiActionsEnhanced.CustomActions": "カスタムアクション", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "パネルに追加", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "キャンセル", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "時間範囲", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "削除", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", - "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "パネルの時間範囲のカスタマイズ", - "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "時間範囲のカスタマイズ", - "xpack.uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "既存のドリルダウンをコピー", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "ドリルダウンにより、パネルと連携する新しい動作を定義できます。複数のアクションを追加し、デフォルトフィルターを無効化できます。", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "非表示", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "ドキュメントを表示", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle": "ドリルダウンを作成", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel": "ドリルダウンを削除", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle": "ドリルダウンを編集", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError": "不十分なライセンスレベル", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType": "ドリルダウンタイプ{type}が存在しません", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText": "テストする前にダッシュボードを保存してください。", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle": "ドリルダウン「{drilldownName}」が作成されました", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText": "テストする前にダッシュボードを保存してください。", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle": "ドリルダウンが削除されました", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText": "テストする前にダッシュボードを保存してください。", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle": "ドリルダウン「{drilldownName}」が更新されました", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle": "ドリルダウンの保存エラー", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText": "テストする前にダッシュボードを保存してください。", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle": "{n}個のドリルダウンが削除されました", - "xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel": "戻る", - "xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel": "閉じる", - "xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton": "ドリルダウンを作成", - "xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title": "ドリルダウンを作成", - "xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body": "{count, number} {count, plural, other {個のドリルダウン}}がコピーされました。", - "xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss": "閉じる", - "xpack.uiActionsEnhanced.drilldowns.containers.DrilldownManager.createNew": "新規作成", - "xpack.uiActionsEnhanced.drilldowns.containers.DrilldownManager.manage": "管理", - "xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton": "保存", - "xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title": "ドリルダウンを編集", - "xpack.uiActionsEnhanced.drilldowns.drilldownManager.state.defaultTitle": "ドリルダウン", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions": "その他のオプション", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "変数を追加", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription": "有効な場合、URLはパーセントエンコーディングを使用してエスケープされます", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl": "URLのエンコード", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel": "新しいウィンドウで開く", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText": "プレビュー\\{\\{event.*\\}\\}では、変数にダミー値が代入されます。", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel": "URLプレビュー:", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText": "プレビュー", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel": "URLを入力", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText": "例:{exampleUrl}", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "構文ヘルプ", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "変数をフィルター", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "ヘルプ", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "無効な形式:{message}", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "無効なフォーマット。例:{exampleUrl}", "xpack.upgradeAssistant.app.deniedPrivilegeDescription": "アップグレードアシスタントを使用して、廃止予定の問題を解決するには、すべてのKibanaスペースを管理するためのアクセス権が必要です。", "xpack.upgradeAssistant.app.deniedPrivilegeTitle": "Kibana管理者ロールが必要です", "xpack.upgradeAssistant.appTitle": "アップグレードアシスタント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7cc85737d01c9..af5325c1b9c20 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2812,7 +2812,6 @@ "dataViews.deprecations.scriptedFields.manualStepTwoMessage": "更新 {numberOfIndexPatternsWithScriptedFields} 个具有脚本字段的数据视图以改为使用运行时字段。多数情况下,要迁移现有脚本,您需要将“return ;”更改为“emit();”。至少有一个脚本字段的数据视图:{allTitles}", "dataViews.deprecations.scriptedFieldsMessage": "您具有 {numberOfIndexPatternsWithScriptedFields} 个使用脚本字段的数据视图 ({titlesPreview}...)。脚本字段已过时,将在未来移除。请改为使用运行时脚本。", "dataViews.deprecations.scriptedFieldsTitle": "找到使用脚本字段的数据视图", - "dataViews.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。", "dataViews.fetchFieldErrorTitle": "提取数据视图 {title}(ID:{id})的字段时出错", "dataViews.functions.dataViewLoad.help": "加载数据视图", "dataViews.functions.dataViewLoad.id.help": "要加载的数据视图 ID", @@ -3036,7 +3035,6 @@ "discover.fieldNameIcons.unknownFieldAriaLabel": "未知字段", "discover.fieldNameIcons.versionFieldAriaLabel": "版本字段", "discover.goToDiscoverMainViewButtonText": "前往 Discover 主视图", - "discover.grid.copyToClipBoardButton": "复制到剪贴板", "discover.grid.documentHeader": "文档", "discover.grid.filterFor": "筛留", "discover.grid.filterForAria": "筛留此 {value}", @@ -5158,7 +5156,6 @@ "kibana-react.tableListView.listing.noMatchedItemsMessage": "没有任何{entityNamePlural}匹配您的搜索。", "kibana-react.tableListView.listing.table.actionTitle": "操作", "kibana-react.tableListView.listing.table.editActionDescription": "编辑", - "kibana-react.tableListView.listing.table.editActionName": "编辑", "kibana-react.tableListView.listing.unableToDeleteDangerMessage": "无法删除{entityName}", "kibanaOverview.addData.sampleDataButtonLabel": "试用我们的样例数据", "kibanaOverview.addData.sectionTitle": "采集您的数据", @@ -5694,6 +5691,90 @@ "uiActions.errors.incompatibleAction": "操作不兼容", "uiActions.triggers.rowClickkDescription": "表格行的单击", "uiActions.triggers.rowClickTitle": "表格行单击", + "uiActionsEnhanced.components.actionWizard.betaActionLabel": "公测版", + "uiActionsEnhanced.components.actionWizard.betaActionTooltip": "此操作位于公测版中,可能会有所更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。请通过报告错误或提供其他反馈来帮助我们。", + "uiActionsEnhanced.components.actionWizard.changeButton": "更改", + "uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "许可证级别不够", + "uiActionsEnhanced.components.actionWizard.triggerPickerHelpText": "这是什么?", + "uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip": "确定向下钻取显示在上下文菜单中的时间", + "uiActionsEnhanced.components.actionWizard.triggerPickerLabel": "显示相关选项:", + "uiActionsEnhanced.components.DrilldownForm.betaActionLabel": "公测版", + "uiActionsEnhanced.components.DrilldownForm.betaActionTooltip": "此操作位于公测版中,可能会有所更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。请通过报告错误或提供其他反馈来帮助我们。", + "uiActionsEnhanced.components.DrilldownForm.changeButton": "更改", + "uiActionsEnhanced.components.DrilldownForm.drilldownAction": "操作", + "uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel": "获取更多的操作", + "uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown": "名称", + "uiActionsEnhanced.components.DrilldownForm.trigger": "触发", + "uiActionsEnhanced.components.DrilldownForm.untitledDrilldown": "未命名向下钻取", + "uiActionsEnhanced.components.DrilldownTable.actionColumnTitle": "操作", + "uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel": "复制", + "uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel": "新建", + "uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel": "删除 ({count})", + "uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel": "编辑", + "uiActionsEnhanced.components.DrilldownTable.nameColumnTitle": "名称", + "uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel": "选择此向下钻取", + "uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle": "触发", + "uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle": "操作", + "uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel": "复制 ({count})", + "uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle": "名称", + "uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage": "选择此模板", + "uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction": "复制", + "uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle": "面板", + "uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle": "触发", + "uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "此触发类型不受此面板支持", + "uiActionsEnhanced.components.TriggerPickerItem.unknown": "未知", + "uiActionsEnhanced.CustomActions": "定制操作", + "uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "添加到面板", + "uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "取消", + "uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "时间范围", + "uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "移除", + "uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", + "uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "定制面板时间范围", + "uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "定制时间范围", + "uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "复制现有向下钻取", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "向下钻取允许您定义与面板交互的新行为。您可以添加多个操作并覆盖默认筛选。", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "隐藏", + "uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "查看文档", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle": "创建向下钻取", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel": "删除向下钻取", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle": "编辑向下钻取", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError": "许可证级别不够", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType": "向下钻取类型 {type} 不存在", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText": "在测试前保存仪表板。", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle": "向下钻取“{drilldownName}”已创建", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText": "在测试前保存仪表板。", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle": "向下钻取已删除", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText": "在测试前保存仪表板。", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle": "向下钻取“{drilldownName}”已更新", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle": "保存向下钻取时出错", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText": "在测试前保存仪表板。", + "uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle": "{n} 个向下钻取已删除", + "uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel": "返回", + "uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel": "关闭", + "uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton": "创建向下钻取", + "uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title": "创建向下钻取", + "uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body": "已复制 {count, number} 个{count, plural, other {向下钻取}}。", + "uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss": "关闭", + "uiActionsEnhanced.drilldowns.containers.DrilldownManager.createNew": "新建", + "uiActionsEnhanced.drilldowns.containers.DrilldownManager.manage": "管理", + "uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton": "保存", + "uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title": "编辑向下钻取", + "uiActionsEnhanced.drilldowns.drilldownManager.state.defaultTitle": "向下钻取", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions": "其他选项", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "添加变量", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription": "如果启用,将使用百分比编码转义 URL", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl": "编码 URL", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel": "在新窗口卡中打开", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText": "请注意,在预览模式下,\\{\\{event.*\\}\\} 变量将替换为虚拟值。", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel": "URL 预览:", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText": "预览", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel": "输入 URL", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText": "例如:{exampleUrl}", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "语法帮助", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "筛选变量", + "uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "帮助", + "uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "格式无效:{message}", + "uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "格式无效。例如:{exampleUrl}", "utils.filename.pathWarning": "路径的格式可能不正确;请验证值", "utils.filename.wildcardWarning": "在文件路径中使用通配符可能会影响终端性能", "visDefaultEditor.advancedToggle.advancedLinkLabel": "高级", @@ -12344,8 +12425,6 @@ "xpack.fleet.agentBulkActions.selectAll": "选择所有页面上的所有内容", "xpack.fleet.agentBulkActions.totalAgents": "正在显示 {count, plural, other {# 个代理}}", "xpack.fleet.agentBulkActions.totalAgentsWithLimit": "正在显示 {count} 个代理(共 {total} 个)", - "xpack.fleet.agentBulkActions.unenrollAgents": "取消注册代理", - "xpack.fleet.agentBulkActions.upgradeAgents": "升级代理", "xpack.fleet.agentDetails.actionsButton": "操作", "xpack.fleet.agentDetails.agentDetailsTitle": "代理“{id}”", "xpack.fleet.agentDetails.agentNotFoundErrorDescription": "找不到代理 ID {agentId}", @@ -12368,9 +12447,7 @@ "xpack.fleet.agentDetails.versionLabel": "代理版本", "xpack.fleet.agentDetails.viewAgentListTitle": "查看所有代理", "xpack.fleet.agentDetails.viewDashboardButtonLabel": "查看代理仪表板", - "xpack.fleet.agentDetailsIntegrations.actionsLabel": "操作", "xpack.fleet.agentDetailsIntegrations.inputTypeEndpointText": "终端", - "xpack.fleet.agentDetailsIntegrations.inputTypeLabel": "输入", "xpack.fleet.agentDetailsIntegrations.inputTypeLogText": "日志", "xpack.fleet.agentDetailsIntegrations.inputTypeMetricsText": "指标", "xpack.fleet.agentDetailsIntegrations.viewLogsButton": "查看日志", @@ -26048,18 +26125,14 @@ "xpack.securitySolution.lastEventTime.failSearchDescription": "无法对上次事件时间执行搜索", "xpack.securitySolution.licensing.unsupportedMachineLearningMessage": "您的许可证不支持 Machine Learning。请升级您的许可证。", "xpack.securitySolution.list.backButton": "返回", - "xpack.securitySolution.lists.cancelValueListsUploadTitle": "取消上传", "xpack.securitySolution.lists.closeValueListsModalTitle": "关闭", - "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButton": "上传值列表", "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButtonTooltip": "在字段值与列表中找到的值匹配时,使用值列表创建例外", "xpack.securitySolution.lists.referenceModalCancelButton": "取消", "xpack.securitySolution.lists.referenceModalDeleteButton": "删除值列表", "xpack.securitySolution.lists.referenceModalDescription": "此值列表与 ({referenceCount}) 个例外{referenceCount, plural, other {列表}}关联。移除此列表将移除引用此值列表的所有例外项。", "xpack.securitySolution.lists.referenceModalTitle": "删除值列表", - "xpack.securitySolution.lists.uploadValueListDescription": "上传编写规则例外时要使用的单值列表。", "xpack.securitySolution.lists.uploadValueListExtensionValidationMessage": "文件必须属于以下类型之一:[{fileTypes}]", "xpack.securitySolution.lists.uploadValueListPrompt": "选择或拖放文件", - "xpack.securitySolution.lists.uploadValueListTitle": "上传值列表", "xpack.securitySolution.lists.valueListsExportError": "导出值列表时出错。", "xpack.securitySolution.lists.valueListsForm.ipRadioLabel": "IP 地址", "xpack.securitySolution.lists.valueListsForm.ipRangesRadioLabel": "IP 范围", @@ -26075,11 +26148,7 @@ "xpack.securitySolution.lists.valueListsTable.fileNameColumn": "文件名", "xpack.securitySolution.lists.valueListsTable.title": "值列表", "xpack.securitySolution.lists.valueListsTable.typeColumn": "类型", - "xpack.securitySolution.lists.valueListsTable.uploadDateColumn": "上传日期", - "xpack.securitySolution.lists.valueListsUploadButton": "上传列表", "xpack.securitySolution.lists.valueListsUploadError": "上传值列表时出错。", - "xpack.securitySolution.lists.valueListsUploadSuccess": "值列表“{fileName}”已上传", - "xpack.securitySolution.lists.valueListsUploadSuccessTitle": "值列表已上传", "xpack.securitySolution.management.policiesSelector.globalEntries": "全局条目", "xpack.securitySolution.management.policiesSelector.label": "策略", "xpack.securitySolution.management.policiesSelector.unassignedEntries": "未分配的条目", @@ -27952,8 +28021,6 @@ "xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel": "用作阈值的值数组;“between”和“notBetween”需要两个值,其他则需要一个值。", "xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "告警的标题。", "xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "满足阈值条件的值。", - "xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "匹配文档的数目{thresholdComparator} {threshold}", - "xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "告警“{name}”已匹配查询", "xpack.stackAlerts.esQuery.alertTypeTitle": "Elasticsearch 查询", "xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", "xpack.stackAlerts.esQuery.invalidEsQueryErrorMessage": "[esQuery]:必须是有效的 JSON", @@ -29360,90 +29427,6 @@ "xpack.triggersActionsUI.timeUnits.secondLabel": "{timeValue, plural, other {秒}}", "xpack.triggersActionsUI.typeRegistry.get.missingActionTypeErrorMessage": "对象类型“{id}”未注册。", "xpack.triggersActionsUI.typeRegistry.register.duplicateObjectTypeErrorMessage": "已注册对象类型“{id}”。", - "xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel": "公测版", - "xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip": "此操作位于公测版中,可能会有所更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。请通过报告错误或提供其他反馈来帮助我们。", - "xpack.uiActionsEnhanced.components.actionWizard.changeButton": "更改", - "xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip": "许可证级别不够", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText": "这是什么?", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip": "确定向下钻取显示在上下文菜单中的时间", - "xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel": "显示相关选项:", - "xpack.uiActionsEnhanced.components.DrilldownForm.betaActionLabel": "公测版", - "xpack.uiActionsEnhanced.components.DrilldownForm.betaActionTooltip": "此操作位于公测版中,可能会有所更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。请通过报告错误或提供其他反馈来帮助我们。", - "xpack.uiActionsEnhanced.components.DrilldownForm.changeButton": "更改", - "xpack.uiActionsEnhanced.components.DrilldownForm.drilldownAction": "操作", - "xpack.uiActionsEnhanced.components.DrilldownForm.getMoreActionsLinkLabel": "获取更多的操作", - "xpack.uiActionsEnhanced.components.DrilldownForm.nameOfDrilldown": "名称", - "xpack.uiActionsEnhanced.components.DrilldownForm.trigger": "触发", - "xpack.uiActionsEnhanced.components.DrilldownForm.untitledDrilldown": "未命名向下钻取", - "xpack.uiActionsEnhanced.components.DrilldownTable.actionColumnTitle": "操作", - "xpack.uiActionsEnhanced.components.DrilldownTable.copyDrilldownButtonLabel": "复制", - "xpack.uiActionsEnhanced.components.DrilldownTable.createDrilldownButtonLabel": "新建", - "xpack.uiActionsEnhanced.components.DrilldownTable.deleteDrilldownsButtonLabel": "删除 ({count})", - "xpack.uiActionsEnhanced.components.DrilldownTable.editDrilldownButtonLabel": "编辑", - "xpack.uiActionsEnhanced.components.DrilldownTable.nameColumnTitle": "名称", - "xpack.uiActionsEnhanced.components.DrilldownTable.selectThisDrilldownCheckboxLabel": "选择此向下钻取", - "xpack.uiActionsEnhanced.components.DrilldownTable.triggerColumnTitle": "触发", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.actionColumnTitle": "操作", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.copyButtonLabel": "复制 ({count})", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.nameColumnTitle": "名称", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.selectableMessage": "选择此模板", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.singleItemCopyAction": "复制", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.sourceColumnTitle": "面板", - "xpack.uiActionsEnhanced.components.DrilldownTemplateTable.triggerColumnTitle": "触发", - "xpack.uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "此触发类型不受此面板支持", - "xpack.uiActionsEnhanced.components.TriggerPickerItem.unknown": "未知", - "xpack.uiActionsEnhanced.CustomActions": "定制操作", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "添加到面板", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "取消", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "时间范围", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "移除", - "xpack.uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", - "xpack.uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "定制面板时间范围", - "xpack.uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "定制时间范围", - "xpack.uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "复制现有向下钻取", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "向下钻取允许您定义与面板交互的新行为。您可以添加多个操作并覆盖默认筛选。", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "隐藏", - "xpack.uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel": "查看文档", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle": "创建向下钻取", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel": "删除向下钻取", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle": "编辑向下钻取", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError": "许可证级别不够", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType": "向下钻取类型 {type} 不存在", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText": "在测试前保存仪表板。", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle": "向下钻取“{drilldownName}”已创建", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText": "在测试前保存仪表板。", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle": "向下钻取已删除", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText": "在测试前保存仪表板。", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle": "向下钻取“{drilldownName}”已更新", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle": "保存向下钻取时出错", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText": "在测试前保存仪表板。", - "xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle": "{n} 个向下钻取已删除", - "xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.BackButtonLabel": "返回", - "xpack.uiActionsEnhanced.drilldowns.components.FlyoutFrame.CloseButtonLabel": "关闭", - "xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.primaryButton": "创建向下钻取", - "xpack.uiActionsEnhanced.drilldowns.containers.createDrilldownForm.title": "创建向下钻取", - "xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.body": "已复制 {count, number} 个{count, plural, other {向下钻取}}。", - "xpack.uiActionsEnhanced.drilldowns.containers.drilldownList.copyingNotification.dismiss": "关闭", - "xpack.uiActionsEnhanced.drilldowns.containers.DrilldownManager.createNew": "新建", - "xpack.uiActionsEnhanced.drilldowns.containers.DrilldownManager.manage": "管理", - "xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.primaryButton": "保存", - "xpack.uiActionsEnhanced.drilldowns.containers.editDrilldownForm.title": "编辑向下钻取", - "xpack.uiActionsEnhanced.drilldowns.drilldownManager.state.defaultTitle": "向下钻取", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.additionalOptions": "其他选项", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle": "添加变量", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeDescription": "如果启用,将使用百分比编码转义 URL", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.encodeUrl": "编码 URL", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel": "在新窗口卡中打开", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText": "请注意,在预览模式下,\\{\\{event.*\\}\\} 变量将替换为虚拟值。", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel": "URL 预览:", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText": "预览", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel": "输入 URL", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText": "例如:{exampleUrl}", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText": "语法帮助", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText": "筛选变量", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "帮助", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "格式无效:{message}", - "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "格式无效。例如:{exampleUrl}", "xpack.upgradeAssistant.app.deniedPrivilegeDescription": "要使用升级助手并解决弃用问题,必须具有管理所有 Kibana 工作区的访问权限。", "xpack.upgradeAssistant.app.deniedPrivilegeTitle": "需要 Kibana 管理员角色", "xpack.upgradeAssistant.appTitle": "升级助手", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index a2bd5cc7b3714..06d5754c5ed4e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -13,7 +13,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFormLabel } from '@elastic/eui'; import { coreMock } from '@kbn/core/public/mocks'; import RuleAdd from './rule_add'; -import { createRule } from '../../lib/rule_api'; +import { createRule, alertingFrameworkHealth } from '../../lib/rule_api'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { Rule, @@ -28,6 +28,8 @@ import { ReactWrapper } from 'enzyme'; import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { useKibana } from '../../../common/lib/kibana'; import { triggersActionsUiConfig } from '../../../common/lib/config_api'; +import { triggersActionsUiHealth } from '../../../common/lib/health_api'; +import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; jest.mock('../../../common/lib/kibana'); @@ -48,9 +50,13 @@ jest.mock('../../../common/lib/health_api', () => ({ triggersActionsUiHealth: jest.fn(() => ({ isRulesAvailable: true })), })); +jest.mock('../../lib/action_connector_api', () => ({ + loadActionTypes: jest.fn(), + loadAllActions: jest.fn(), +})); + const actionTypeRegistry = actionTypeRegistryMock.create(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); -const useKibanaMock = useKibana as jest.Mocked; export const TestExpression: FunctionComponent = () => { return ( @@ -65,13 +71,23 @@ export const TestExpression: FunctionComponent = () => { }; describe('rule_add', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); let wrapper: ReactWrapper; async function setup( initialValues?: Partial, onClose: RuleAddProps['onClose'] = jest.fn(), - defaultScheduleInterval?: string + defaultScheduleInterval?: string, + ruleTypeId?: string, + actionsShow: boolean = false ) { + const useKibanaMock = useKibana as jest.Mocked; const mocks = coreMock.createSetup(); const { loadRuleTypes } = jest.requireMock('../../lib/rule_api'); const ruleTypes = [ @@ -114,6 +130,9 @@ describe('rule_add', () => { save: true, delete: true, }, + actions: { + show: actionsShow, + }, }; mocks.http.get.mockResolvedValue({ @@ -165,6 +184,7 @@ describe('rule_add', () => { actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} metadata={{ test: 'some value', fields: ['test'] }} + ruleTypeId={ruleTypeId} /> ); @@ -182,6 +202,11 @@ describe('rule_add', () => { const onClose = jest.fn(); await setup({}, onClose); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="addRuleFlyoutTitle"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="saveRuleButton"]').exists()).toBeTruthy(); @@ -273,7 +298,7 @@ describe('rule_add', () => { (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ minimumScheduleInterval: { value: '1m', enforce: false }, }); - await setup({ ruleTypeId: 'my-rule-type' }, jest.fn(), '3h'); + await setup({ ruleTypeId: 'my-rule-type' }, jest.fn(), '3h', 'my-rule-type', true); // Wait for handlers to fire await act(async () => { @@ -290,6 +315,39 @@ describe('rule_add', () => { expect(intervalInputUnit).toBe('h'); expect(intervalInput).toBe(3); }); + + it('should load connectors and connector types when there is a pre-selected rule type', async () => { + (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ + minimumScheduleInterval: { value: '1m', enforce: false }, + }); + + await setup({}, jest.fn(), undefined, 'my-rule-type', true); + + expect(triggersActionsUiHealth).toHaveBeenCalledTimes(1); + expect(alertingFrameworkHealth).toHaveBeenCalledTimes(1); + expect(loadActionTypes).toHaveBeenCalledTimes(1); + expect(loadAllActions).toHaveBeenCalledTimes(1); + }); + + it('should not load connectors and connector types when there is not an encryptionKey', async () => { + (triggersActionsUiConfig as jest.Mock).mockResolvedValue({ + minimumScheduleInterval: { value: '1m', enforce: false }, + }); + (alertingFrameworkHealth as jest.Mock).mockResolvedValue({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: false, + }); + + await setup({}, jest.fn(), undefined, 'my-rule-type', true); + + expect(triggersActionsUiHealth).toHaveBeenCalledTimes(1); + expect(alertingFrameworkHealth).toHaveBeenCalledTimes(1); + expect(loadActionTypes).not.toHaveBeenCalled(); + expect(loadAllActions).not.toHaveBeenCalled(); + expect(wrapper.find('[data-test-subj="actionNeededEmptyPrompt"]').first().text()).toContain( + 'You must configure an encryption key to use Alerting' + ); + }); }); function mockRule(overloads: Partial = {}): Rule { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index d9e275e21130d..f89b3f91f150c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -245,7 +245,7 @@ const RuleAdd = ({ - + /x-pack/plugins/ui_actions_enhanced'], - coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/ui_actions_enhanced', - coverageReporters: ['text', 'html'], - collectCoverageFrom: [ - '/x-pack/plugins/ui_actions_enhanced/{common,public,server}/**/*.{ts,tsx}', - ], -}; diff --git a/x-pack/plugins/ui_actions_enhanced/tsconfig.json b/x-pack/plugins/ui_actions_enhanced/tsconfig.json deleted file mode 100644 index 100a1decd9427..0000000000000 --- a/x-pack/plugins/ui_actions_enhanced/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "public/**/*", - "server/**/*", - "common/**/*", - "../../../typings/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, - - { "path": "../licensing/tsconfig.json" }, - ] -} diff --git a/x-pack/plugins/ux/common/elasticsearch_fieldnames.ts b/x-pack/plugins/ux/common/elasticsearch_fieldnames.ts index 9bfc0f7596d09..aad4b9f13979c 100644 --- a/x-pack/plugins/ux/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/ux/common/elasticsearch_fieldnames.ts @@ -13,9 +13,17 @@ export const AGENT = 'agent'; export const AGENT_NAME = 'agent.name'; export const AGENT_VERSION = 'agent.version'; +export const ERROR_EXC_MESSAGE = 'error.exception.message'; +export const ERROR_EXC_TYPE = 'error.exception.type'; +export const ERROR_GROUP_ID = 'error.grouping_key'; + +export const PROCESSOR_EVENT = 'processor.event'; + export const URL_FULL = 'url.full'; export const USER_AGENT_NAME = 'user_agent.name'; +export const SERVICE_LANGUAGE_NAME = 'service.language.name'; + export const TRANSACTION_DURATION = 'transaction.duration.us'; export const TRANSACTION_TYPE = 'transaction.type'; export const TRANSACTION_RESULT = 'transaction.result'; diff --git a/x-pack/plugins/ux/e2e/journeys/index.ts b/x-pack/plugins/ux/e2e/journeys/index.ts index a9b3851a8d589..5800960d57be1 100644 --- a/x-pack/plugins/ux/e2e/journeys/index.ts +++ b/x-pack/plugins/ux/e2e/journeys/index.ts @@ -6,3 +6,4 @@ */ export * from './url_ux_query.journey'; +export * from './ux_js_errors.journey'; diff --git a/x-pack/plugins/ux/e2e/journeys/ux_js_errors.journey.ts b/x-pack/plugins/ux/e2e/journeys/ux_js_errors.journey.ts new file mode 100644 index 0000000000000..eb61a6e446013 --- /dev/null +++ b/x-pack/plugins/ux/e2e/journeys/ux_js_errors.journey.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before } from '@elastic/synthetics'; +import { UXDashboardDatePicker } from '../page_objects/date_picker'; +import { byTestId, loginToKibana, waitForLoadingToFinish } from './utils'; + +const jsErrorCount = '3 k'; +const jsErrorLabel = `Total errors + +${jsErrorCount}`; + +journey('UX JsErrors', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const queryParams = { + percentile: '50', + rangeFrom: '2020-05-18T11:51:00.000Z', + rangeTo: '2021-10-30T06:37:15.536Z', + }; + const queryString = new URLSearchParams(queryParams).toString(); + + const baseUrl = `${params.kibanaUrl}/app/ux`; + + step('Go to UX Dashboard', async () => { + await page.goto(`${baseUrl}?${queryString}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ + page, + user: { username: 'viewer_user', password: 'changeme' }, + }); + }); + + step('Set date range', async () => { + const datePickerPage = new UXDashboardDatePicker(page); + await datePickerPage.setDefaultE2eRange(); + }); + + step('Confirm error count', async () => { + // Wait until chart data is loaded + page.waitForLoadState('networkidle'); + await page.waitForSelector(`text=${jsErrorCount}`); + + const jsErrors = await ( + await page.waitForSelector(byTestId('uxJsErrorsTotal')) + ).innerText(); + + expect(jsErrors).toBe(jsErrorLabel); + }); +}); diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/impactful_metrics/js_errors.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/impactful_metrics/js_errors.tsx index 1085e7e5952a6..cb491ac4c02b2 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/impactful_metrics/js_errors.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/impactful_metrics/js_errors.tsx @@ -19,9 +19,8 @@ import { import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { FETCH_STATUS } from '@kbn/observability-plugin/public'; +import { useJsErrorsQuery } from '../../../../hooks/use_js_errors_query'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { useFetcher } from '../../../../hooks/use_fetcher'; import { useKibanaServices } from '../../../../hooks/use_kibana_services'; import { I18LABELS } from '../translations'; import { CsmSharedContext } from '../csm_shared_context'; @@ -35,34 +34,13 @@ interface JSErrorItem { export function JSErrors() { const { http } = useKibanaServices(); const basePath = http.basePath.get(); - const { rangeId, urlParams, uxUiFilters } = useLegacyUrlParams(); - - const { start, end, serviceName, searchTerm } = urlParams; + const { + urlParams: { serviceName }, + } = useLegacyUrlParams(); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 }); - const { data, status } = useFetcher( - (callApmApi) => { - if (start && end && serviceName) { - return callApmApi('GET /internal/apm/ux/js-errors', { - params: { - query: { - start, - end, - urlQuery: searchTerm || undefined, - uiFilters: JSON.stringify(uxUiFilters), - pageSize: String(pagination.pageSize), - pageIndex: String(pagination.pageIndex), - }, - }, - }); - } - return Promise.resolve(null); - }, - // `rangeId` acts as a cache buster for stable ranges like "Today" - // eslint-disable-next-line react-hooks/exhaustive-deps - [start, end, serviceName, uxUiFilters, pagination, searchTerm, rangeId] - ); + const { data, loading } = useJsErrorsQuery(pagination); const { sharedData: { totalPageViews }, @@ -130,16 +108,16 @@ export function JSErrors() { ) } description={I18LABELS.totalErrors} - isLoading={status === FETCH_STATUS.LOADING} + isLoading={!!loading} /> { + if (!esQueryResponse) return {}; + + const { totalErrorGroups, totalErrorPages, errors } = + esQueryResponse?.aggregations ?? {}; + + return { + totalErrorPages: totalErrorPages?.value ?? 0, + totalErrors: esQueryResponse.hits.total ?? 0, + totalErrorGroups: totalErrorGroups?.value ?? 0, + items: errors?.buckets.map(({ sample, key, impactedPages }: any) => { + return { + count: impactedPages.pageCount.value, + errorGroupId: key, + errorMessage: ( + sample.hits.hits[0]._source as { + error: { exception: Array<{ message: string }> }; + } + ).error.exception?.[0].message, + }; + }), + }; + }, [esQueryResponse]); + + return { data, loading }; +} diff --git a/x-pack/plugins/ux/public/services/data/__snapshots__/js_errors_query.test.ts.snap b/x-pack/plugins/ux/public/services/data/__snapshots__/js_errors_query.test.ts.snap new file mode 100644 index 0000000000000..94608afe29404 --- /dev/null +++ b/x-pack/plugins/ux/public/services/data/__snapshots__/js_errors_query.test.ts.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jsErrorsQuery fetches js errors 1`] = ` +Object { + "body": Object { + "aggs": Object { + "errors": Object { + "aggs": Object { + "bucket_truncate": Object { + "bucket_sort": Object { + "from": 0, + "size": 5, + }, + }, + "impactedPages": Object { + "aggs": Object { + "pageCount": Object { + "cardinality": Object { + "field": "transaction.id", + }, + }, + }, + "filter": Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + }, + "sample": Object { + "top_hits": Object { + "_source": Array [ + "error.exception.message", + "error.exception.type", + "error.grouping_key", + "@timestamp", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": "desc", + }, + ], + }, + }, + }, + "terms": Object { + "field": "error.grouping_key", + "size": 500, + }, + }, + "totalErrorGroups": Object { + "cardinality": Object { + "field": "error.grouping_key", + }, + }, + "totalErrorPages": Object { + "cardinality": Object { + "field": "transaction.id", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 0, + "lte": 50000, + }, + }, + }, + Object { + "term": Object { + "agent.name": "rum-js", + }, + }, + Object { + "term": Object { + "service.language.name": "javascript", + }, + }, + Object { + "terms": Object { + "processor.event": Array [ + "error", + ], + }, + }, + ], + "must_not": Array [], + }, + }, + "size": 0, + "track_total_hits": true, + }, +} +`; + diff --git a/x-pack/plugins/ux/public/services/data/js_errors_query.test.ts b/x-pack/plugins/ux/public/services/data/js_errors_query.test.ts new file mode 100644 index 0000000000000..b6203c59ca602 --- /dev/null +++ b/x-pack/plugins/ux/public/services/data/js_errors_query.test.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 { jsErrorsQuery } from './js_errors_query'; + +describe('jsErrorsQuery', () => { + it('fetches js errors', () => { + const query = jsErrorsQuery(0, 50000, 5, 0, '', { + environment: 'ENVIRONMENT_ALL', + }); + expect(query).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/server/routes/rum_client/get_js_errors.ts b/x-pack/plugins/ux/public/services/data/js_errors_query.ts similarity index 63% rename from x-pack/plugins/apm/server/routes/rum_client/get_js_errors.ts rename to x-pack/plugins/ux/public/services/data/js_errors_query.ts index a0f7c0940b812..612cf93817da0 100644 --- a/x-pack/plugins/apm/server/routes/rum_client/get_js_errors.ts +++ b/x-pack/plugins/ux/public/services/data/js_errors_query.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { mergeProjection } from '../../projections/util/merge_projection'; -import { SetupUX } from './route'; -import { getRumErrorsProjection } from '../../projections/rum_page_load_transactions'; +import { mergeProjection } from '../../../common/utils/merge_projection'; +import { SetupUX, UxUIFilters } from '../../../typings/ui_filters'; import { ERROR_EXC_MESSAGE, ERROR_EXC_TYPE, @@ -16,22 +15,17 @@ import { TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; +import { getRumErrorsProjection } from './projections'; -export async function getJSErrors({ - setup, - pageSize, - pageIndex, - urlQuery, - start, - end, -}: { - setup: SetupUX; - pageSize: number; - pageIndex: number; - urlQuery?: string; - start: number; - end: number; -}) { +export function jsErrorsQuery( + start: number, + end: number, + pageSize: number, + pageIndex: number, + urlQuery?: string, + uiFilters?: UxUIFilters +) { + const setup: SetupUX = { uiFilters: uiFilters ? uiFilters : {} }; const projection = getRumErrorsProjection({ setup, urlQuery, @@ -98,27 +92,5 @@ export async function getJSErrors({ }, }); - const { apmEventClient } = setup; - - const response = await apmEventClient.search('get_js_errors', params); - - const { totalErrorGroups, totalErrorPages, errors } = - response.aggregations ?? {}; - - return { - totalErrorPages: totalErrorPages?.value ?? 0, - totalErrors: response.hits.total.value ?? 0, - totalErrorGroups: totalErrorGroups?.value ?? 0, - items: errors?.buckets.map(({ sample, key, impactedPages }) => { - return { - count: impactedPages.pageCount.value, - errorGroupId: key, - errorMessage: ( - sample.hits.hits[0]._source as { - error: { exception: Array<{ message: string }> }; - } - ).error.exception?.[0].message, - }; - }), - }; + return params; } diff --git a/x-pack/plugins/ux/public/services/data/projections.ts b/x-pack/plugins/ux/public/services/data/projections.ts index b860e700dc241..89c3f3e8a52b9 100644 --- a/x-pack/plugins/ux/public/services/data/projections.ts +++ b/x-pack/plugins/ux/public/services/data/projections.ts @@ -4,12 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; import { SetupUX } from '../../../typings/ui_filters'; import { getEsFilter } from './get_es_filter'; import { rangeQuery } from './range_query'; +import { + AGENT_NAME, + SERVICE_LANGUAGE_NAME, + PROCESSOR_EVENT, +} from '../../../common/elasticsearch_fieldnames'; export function getRumPageLoadTransactionsProjection({ setup, @@ -66,3 +72,60 @@ export function getRumPageLoadTransactionsProjection({ }, }; } + +export interface RumErrorsProjection { + body: { + query: { + bool: { + filter: QueryDslQueryContainer[]; + must_not: QueryDslQueryContainer[]; + }; + }; + }; +} + +export function getRumErrorsProjection({ + setup, + urlQuery, + start, + end, +}: { + setup: SetupUX; + urlQuery?: string; + start: number; + end: number; +}): RumErrorsProjection { + return { + body: { + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + { term: { [AGENT_NAME]: 'rum-js' } }, + { + term: { + [SERVICE_LANGUAGE_NAME]: 'javascript', + }, + }, + { + terms: { + [PROCESSOR_EVENT]: [ProcessorEvent.error], + }, + }, + ...getEsFilter(setup.uiFilters), + ...(urlQuery + ? [ + { + wildcard: { + 'url.full': `*${urlQuery}*`, + }, + }, + ] + : []), + ], + must_not: [...getEsFilter(setup.uiFilters, true)], + }, + }, + }, + }; +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts index 2f2ccc4f4f382..8f10635975ba0 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/index.ts @@ -10,6 +10,6 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { describe('es_query', () => { - loadTestFile(require.resolve('./alert')); + loadTestFile(require.resolve('./rule')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts similarity index 63% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts index cf2cb8c6b0f1c..35a6f296565ed 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/rule.ts @@ -17,19 +17,19 @@ import { } from '../../../../../common/lib'; import { createEsDocuments } from '../lib/create_test_data'; -const ALERT_TYPE_ID = '.es-query'; -const ACTION_TYPE_ID = '.index'; -const ES_TEST_INDEX_SOURCE = 'builtin-alert:es-query'; +const RULE_TYPE_ID = '.es-query'; +const CONNECTOR_TYPE_ID = '.index'; +const ES_TEST_INDEX_SOURCE = 'builtin-rule:es-query'; const ES_TEST_INDEX_REFERENCE = '-na-'; const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-output`; -const ALERT_INTERVALS_TO_WRITE = 5; -const ALERT_INTERVAL_SECONDS = 3; -const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000; +const RULE_INTERVALS_TO_WRITE = 5; +const RULE_INTERVAL_SECONDS = 4; +const RULE_INTERVAL_MILLIS = RULE_INTERVAL_SECONDS * 1000; const ES_GROUPS_TO_WRITE = 3; // eslint-disable-next-line import/no-default-export -export default function alertTests({ getService }: FtrProviderContext) { +export default function ruleTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); const indexPatterns = getService('indexPatterns'); @@ -37,10 +37,9 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); - // FLAKY: https://github.com/elastic/kibana/issues/129380 - describe.skip('alert', async () => { + describe('rule', async () => { let endDate: string; - let actionId: string; + let connectorId: string; const objectRemover = new ObjectRemover(supertest); beforeEach(async () => { @@ -50,10 +49,10 @@ export default function alertTests({ getService }: FtrProviderContext) { await esTestIndexToolOutput.destroy(); await esTestIndexToolOutput.setup(); - actionId = await createAction(supertest, objectRemover); + connectorId = await createConnector(supertest, objectRemover); // write documents in the future, figure out the end date - const endDateMillis = Date.now() + (ALERT_INTERVALS_TO_WRITE - 1) * ALERT_INTERVAL_MILLIS; + const endDateMillis = Date.now() + (RULE_INTERVALS_TO_WRITE - 1) * RULE_INTERVAL_MILLIS; endDate = new Date(endDateMillis).toISOString(); }); @@ -67,14 +66,14 @@ export default function alertTests({ getService }: FtrProviderContext) { [ 'esQuery', async () => { - await createAlert({ + await createRule({ name: 'never fire', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, thresholdComparator: '<', threshold: [0], }); - await createAlert({ + await createRule({ name: 'always fire', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, @@ -91,7 +90,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { override: true }, getUrlPrefix(Spaces.space1.id) ); - await createAlert({ + await createRule({ name: 'never fire', size: 100, thresholdComparator: '<', @@ -106,7 +105,7 @@ export default function alertTests({ getService }: FtrProviderContext) { filter: [], }, }); - await createAlert({ + await createRule({ name: 'always fire', size: 100, thresholdComparator: '>', @@ -126,7 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) { ].forEach(([searchType, initData]) => it(`runs correctly: threshold on hit count < > for ${searchType} search type`, async () => { // write documents from now to the future end date in groups - createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); await initData(); const docs = await waitForDocs(2); @@ -136,14 +135,14 @@ export default function alertTests({ getService }: FtrProviderContext) { const { name, title, message } = doc._source.params; expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); + expect(title).to.be(`rule 'always fire' matched query`); const messagePattern = - /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + /rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; expect(message).to.match(messagePattern); expect(hits).not.to.be.empty(); // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution + // since this rule always fires, the latestTimestamp value should be updated each execution if (!i) { expect(previousTimestamp).to.be.empty(); } else { @@ -157,7 +156,7 @@ export default function alertTests({ getService }: FtrProviderContext) { [ 'esQuery', async () => { - await createAlert({ + await createRule({ name: 'never fire', size: 100, thresholdComparator: '<', @@ -165,7 +164,7 @@ export default function alertTests({ getService }: FtrProviderContext) { timeField: 'date_epoch_millis', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, }); - await createAlert({ + await createRule({ name: 'always fire', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, @@ -183,7 +182,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { override: true }, getUrlPrefix(Spaces.space1.id) ); - await createAlert({ + await createRule({ name: 'never fire', size: 100, thresholdComparator: '<', @@ -198,7 +197,7 @@ export default function alertTests({ getService }: FtrProviderContext) { filter: [], }, }); - await createAlert({ + await createRule({ name: 'always fire', size: 100, thresholdComparator: '>', @@ -218,7 +217,7 @@ export default function alertTests({ getService }: FtrProviderContext) { ].forEach(([searchType, initData]) => it(`runs correctly: use epoch millis - threshold on hit count < > for ${searchType} search type`, async () => { // write documents from now to the future end date in groups - createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); await initData(); const docs = await waitForDocs(2); @@ -228,14 +227,14 @@ export default function alertTests({ getService }: FtrProviderContext) { const { name, title, message } = doc._source.params; expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); + expect(title).to.be(`rule 'always fire' matched query`); const messagePattern = - /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + /rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; expect(message).to.match(messagePattern); expect(hits).not.to.be.empty(); // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution + // since this rule always fires, the latestTimestamp value should be updated each execution if (!i) { expect(previousTimestamp).to.be.empty(); } else { @@ -266,17 +265,17 @@ export default function alertTests({ getService }: FtrProviderContext) { }, }; }; - await createAlert({ + await createRule({ name: 'never fire', - esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)), + esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * RULE_INTERVALS_TO_WRITE + 1)), size: 100, thresholdComparator: '<', threshold: [-1], }); - await createAlert({ + await createRule({ name: 'fires once', esQuery: JSON.stringify( - rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2)) + rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * RULE_INTERVALS_TO_WRITE) / 2)) ), size: 100, thresholdComparator: '>=', @@ -292,7 +291,7 @@ export default function alertTests({ getService }: FtrProviderContext) { { override: true }, getUrlPrefix(Spaces.space1.id) ); - await createAlert({ + await createRule({ name: 'never fire', size: 100, thresholdComparator: '<', @@ -300,14 +299,14 @@ export default function alertTests({ getService }: FtrProviderContext) { searchType: 'searchSource', searchConfiguration: { query: { - query: `testedValue > ${ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1}`, + query: `testedValue > ${ES_GROUPS_TO_WRITE * RULE_INTERVALS_TO_WRITE + 1}`, language: 'kuery', }, index: esTestDataView.id, filter: [], }, }); - await createAlert({ + await createRule({ name: 'fires once', size: 100, thresholdComparator: '>=', @@ -316,7 +315,7 @@ export default function alertTests({ getService }: FtrProviderContext) { searchConfiguration: { query: { query: `testedValue > ${Math.floor( - (ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2 + (ES_GROUPS_TO_WRITE * RULE_INTERVALS_TO_WRITE) / 2 )}`, language: 'kuery', }, @@ -329,7 +328,7 @@ export default function alertTests({ getService }: FtrProviderContext) { ].forEach(([searchType, initData]) => it(`runs correctly with query: threshold on hit count < > for ${searchType}`, async () => { // write documents from now to the future end date in groups - createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); await initData(); const docs = await waitForDocs(1); @@ -338,9 +337,9 @@ export default function alertTests({ getService }: FtrProviderContext) { const { name, title, message } = doc._source.params; expect(name).to.be('fires once'); - expect(title).to.be(`alert 'fires once' matched query`); + expect(title).to.be(`rule 'fires once' matched query`); const messagePattern = - /alert 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + /rule 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; expect(message).to.match(messagePattern); expect(hits).not.to.be.empty(); expect(previousTimestamp).to.be.empty(); @@ -352,7 +351,7 @@ export default function alertTests({ getService }: FtrProviderContext) { [ 'esQuery', async () => { - await createAlert({ + await createRule({ name: 'always fire', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, @@ -370,7 +369,7 @@ export default function alertTests({ getService }: FtrProviderContext) { getUrlPrefix(Spaces.space1.id) ); - await createAlert({ + await createRule({ name: 'always fire', size: 100, thresholdComparator: '<', @@ -398,14 +397,14 @@ export default function alertTests({ getService }: FtrProviderContext) { const { name, title, message } = doc._source.params; expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); + expect(title).to.be(`rule 'always fire' matched query`); const messagePattern = - /alert 'always fire' is active:\n\n- Value: 0+\n- Conditions Met: Number of matching documents is less than 1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + /rule 'always fire' is active:\n\n- Value: 0+\n- Conditions Met: Number of matching documents is less than 1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; expect(message).to.match(messagePattern); expect(hits).to.be.empty(); // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution + // since this rule always fires, the latestTimestamp value should be updated each execution if (!i) { expect(previousTimestamp).to.be.empty(); } else { @@ -415,13 +414,98 @@ export default function alertTests({ getService }: FtrProviderContext) { }) ); + [ + [ + 'esQuery', + async () => { + // This rule should be active initially when the number of documents is below the threshold + // and then recover when we add more documents. + await createRule({ + name: 'fire then recovers', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '<', + threshold: [1], + notifyWhen: 'onActionGroupChange', + timeWindowSize: RULE_INTERVAL_SECONDS, + }); + }, + ] as const, + [ + 'searchSource', + async () => { + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + // This rule should be active initially when the number of documents is below the threshold + // and then recover when we add more documents. + await createRule({ + name: 'fire then recovers', + size: 100, + thresholdComparator: '<', + threshold: [1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + notifyWhen: 'onActionGroupChange', + timeWindowSize: RULE_INTERVAL_SECONDS, + }); + }, + ] as const, + ].forEach(([searchType, initData]) => + it(`runs correctly and populates recovery context for ${searchType} search type`, async () => { + await initData(); + + // delay to let rule run once before adding data + await new Promise((resolve) => setTimeout(resolve, 3000)); + await createEsDocumentsInGroups(1); + + const docs = await waitForDocs(2); + const activeDoc = docs[0]; + const { + name: activeName, + title: activeTitle, + value: activeValue, + message: activeMessage, + } = activeDoc._source.params; + + expect(activeName).to.be('fire then recovers'); + expect(activeTitle).to.be(`rule 'fire then recovers' matched query`); + expect(activeValue).to.be('0'); + expect(activeMessage).to.match( + /rule 'fire then recovers' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is less than 1 over 4s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/ + ); + + const recoveredDoc = docs[1]; + const { + name: recoveredName, + title: recoveredTitle, + message: recoveredMessage, + } = recoveredDoc._source.params; + + expect(recoveredName).to.be('fire then recovers'); + expect(recoveredTitle).to.be(`rule 'fire then recovers' recovered`); + expect(recoveredMessage).to.match( + /rule 'fire then recovers' is recovered:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is NOT less than 1 over 4s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\n- Link:/ + ); + }) + ); + async function createEsDocumentsInGroups(groups: number) { await createEsDocuments( es, esTestIndexTool, endDate, - ALERT_INTERVALS_TO_WRITE, - ALERT_INTERVAL_MILLIS, + RULE_INTERVALS_TO_WRITE, + RULE_INTERVAL_MILLIS, groups ); } @@ -434,7 +518,7 @@ export default function alertTests({ getService }: FtrProviderContext) { ); } - interface CreateAlertParams { + interface CreateRuleParams { name: string; size: number; thresholdComparator: string; @@ -444,11 +528,12 @@ export default function alertTests({ getService }: FtrProviderContext) { timeField?: string; searchConfiguration?: unknown; searchType?: 'searchSource'; + notifyWhen?: string; } - async function createAlert(params: CreateAlertParams): Promise { + async function createRule(params: CreateRuleParams): Promise { const action = { - id: actionId, + id: connectorId, group: 'query matched', params: { documents: [ @@ -456,7 +541,7 @@ export default function alertTests({ getService }: FtrProviderContext) { source: ES_TEST_INDEX_SOURCE, reference: ES_TEST_INDEX_REFERENCE, params: { - name: '{{{alertName}}}', + name: '{{{rule.name}}}', value: '{{{context.value}}}', title: '{{{context.title}}}', message: '{{{context.message}}}', @@ -469,7 +554,28 @@ export default function alertTests({ getService }: FtrProviderContext) { }, }; - const alertParams = + const recoveryAction = { + id: connectorId, + group: 'recovered', + params: { + documents: [ + { + source: ES_TEST_INDEX_SOURCE, + reference: ES_TEST_INDEX_REFERENCE, + params: { + name: '{{{rule.name}}}', + value: '{{{context.value}}}', + title: '{{{context.title}}}', + message: '{{{context.message}}}', + }, + hits: '{{context.hits}}', + date: '{{{context.date}}}', + }, + ], + }, + }; + + const ruleParams = params.searchType === 'searchSource' ? { searchConfiguration: params.searchConfiguration, @@ -480,44 +586,44 @@ export default function alertTests({ getService }: FtrProviderContext) { esQuery: params.esQuery, }; - const { body: createdAlert } = await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send({ name: params.name, consumer: 'alerts', enabled: true, - rule_type_id: ALERT_TYPE_ID, - schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` }, - actions: [action], - notify_when: 'onActiveAlert', + rule_type_id: RULE_TYPE_ID, + schedule: { interval: `${RULE_INTERVAL_SECONDS}s` }, + actions: [action, recoveryAction], + notify_when: params.notifyWhen || 'onActiveAlert', params: { size: params.size, - timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5, + timeWindowSize: params.timeWindowSize || RULE_INTERVAL_SECONDS * 5, timeWindowUnit: 's', thresholdComparator: params.thresholdComparator, threshold: params.threshold, searchType: params.searchType, - ...alertParams, + ...ruleParams, }, }) .expect(200); - const alertId = createdAlert.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); + const ruleId = createdRule.id; + objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); - return alertId; + return ruleId; } }); } -async function createAction(supertest: any, objectRemover: ObjectRemover): Promise { - const { body: createdAction } = await supertest +async function createConnector(supertest: any, objectRemover: ObjectRemover): Promise { + const { body: createdConnector } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'index action for es query FT', - connector_type_id: ACTION_TYPE_ID, + connector_type_id: CONNECTOR_TYPE_ID, config: { index: ES_TEST_OUTPUT_INDEX_NAME, }, @@ -525,8 +631,8 @@ async function createAction(supertest: any, objectRemover: ObjectRemover): Promi }) .expect(200); - const actionId = createdAction.id; - objectRemover.add(Spaces.space1.id, actionId, 'connector', 'actions'); + const connectorId = createdConnector.id; + objectRemover.add(Spaces.space1.id, connectorId, 'connector', 'actions'); - return actionId; + return connectorId; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts index 73a81904d0cc0..a234625f01824 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/lib/create_test_data.ts @@ -26,14 +26,16 @@ export async function createEsDocuments( const endDateMillis = Date.parse(endDate) - intervalMillis / 2; let testedValue = 0; + const promises: Array> = []; times(intervals, (interval) => { const date = endDateMillis - interval * intervalMillis; // don't need await on these, wait at the end of the function times(groups, () => { - createEsDocument(es, date, testedValue++); + promises.push(createEsDocument(es, date, testedValue++)); }); }); + await Promise.all(promises); const totalDocuments = intervals * groups; await esTestIndexTool.waitForDocs(DOCUMENT_SOURCE, DOCUMENT_REFERENCE, totalDocuments); @@ -51,6 +53,7 @@ async function createEsDocument(es: Client, epochMillis: number, testedValue: nu const response = await es.index({ id: uuid(), index: ES_TEST_INDEX_NAME, + refresh: 'wait_for', body: document, }); 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 aa0c1af92f951..230ae0e134f43 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 64b2b5211a1bf..5d6f38b368f2e 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/api_integration/apis/ml/jobs/get_groups.ts b/x-pack/test/api_integration/apis/ml/jobs/get_groups.ts new file mode 100644 index 0000000000000..9cccdfe65869c --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/get_groups.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { Group } from '@kbn/ml-plugin/common/types/groups'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { MULTI_METRIC_JOB_CONFIG, SINGLE_METRIC_JOB_CONFIG } from './common_jobs'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; + + const testCalendarsConfigs = [ + { + calendar_id: `test_get_cal_1`, + job_ids: ['multi-metric'], + description: `Test calendar 1`, + }, + { + calendar_id: `test_get_cal_2`, + job_ids: [MULTI_METRIC_JOB_CONFIG.job_id, 'multi-metric'], + description: `Test calendar 2`, + }, + { + calendar_id: `test_get_cal_3`, + job_ids: ['brand-new-group'], + description: `Test calendar 3`, + }, + ]; + + async function runGetGroupsRequest(user: USER, expectedResponsecode: number): Promise { + const { body, status } = await supertest + .get('/api/ml/jobs/groups') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(expectedResponsecode, status, body); + + return body; + } + + const expectedGroups = [ + { + id: 'automated', + jobIds: [MULTI_METRIC_JOB_CONFIG.job_id, SINGLE_METRIC_JOB_CONFIG.job_id], + calendarIds: [], + }, + { + id: 'brand-new-group', + jobIds: [], + calendarIds: ['test_get_cal_3'], + }, + { + id: 'farequote', + jobIds: [MULTI_METRIC_JOB_CONFIG.job_id, SINGLE_METRIC_JOB_CONFIG.job_id], + calendarIds: [], + }, + { + id: 'multi-metric', + jobIds: [MULTI_METRIC_JOB_CONFIG.job_id], + calendarIds: ['test_get_cal_1', 'test_get_cal_2'], + }, + { + id: 'single-metric', + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id], + calendarIds: [], + }, + ]; + + describe('get groups', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const job of testSetupJobConfigs) { + await ml.api.createAnomalyDetectionJob(job); + } + + for (const cal of testCalendarsConfigs) { + await ml.api.createCalendar(cal.calendar_id, cal); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('returns expected list of groups', async () => { + const groups = await runGetGroupsRequest(USER.ML_VIEWER, 200); + expect(groups).to.eql( + expectedGroups, + `response groups list should equal the expected groups ${JSON.stringify(expectedGroups)})` + ); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts index 91368251ff2d7..e530b6bfb3622 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/index.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts @@ -22,5 +22,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./force_start_datafeeds_spaces')); loadTestFile(require.resolve('./stop_datafeeds')); loadTestFile(require.resolve('./stop_datafeeds_spaces')); + loadTestFile(require.resolve('./get_groups')); }); } diff --git a/x-pack/test/api_integration/apis/ml/results/get_partition_fields_values.ts b/x-pack/test/api_integration/apis/ml/results/get_partition_fields_values.ts new file mode 100644 index 0000000000000..6e2ca3b06c4c1 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/results/get_partition_fields_values.ts @@ -0,0 +1,233 @@ +/* + * 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 { Datafeed, Job } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; +import type { PartitionFieldValueResponse } from '@kbn/ml-plugin/server/models/results_service/get_partition_fields_values'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + function getJobConfig(jobId: string, enableModelPlot = true) { + return { + job_id: jobId, + description: + 'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [ + { function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'min', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'max', field_name: 'responsetime', partition_field_name: 'airline' }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: enableModelPlot }, + } as Job; + } + + function getDatafeedConfig(jobId: string) { + return { + datafeed_id: `datafeed-${jobId}`, + indices: ['ft_farequote'], + job_id: jobId, + query: { bool: { must: [{ match_all: {} }] } }, + } as Datafeed; + } + + async function createMockJobs() { + await ml.api.createAndRunAnomalyDetectionLookbackJob( + getJobConfig('fq_multi_1_ae'), + getDatafeedConfig('fq_multi_1_ae') + ); + + await ml.api.createAndRunAnomalyDetectionLookbackJob( + getJobConfig('fq_multi_2_ae', false), + getDatafeedConfig('fq_multi_2_ae') + ); + } + + async function runRequest(requestBody: object): Promise { + const { body, status } = await supertest + .post(`/api/ml/results/partition_fields_values`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody); + ml.api.assertResponseStatusCode(200, status, body); + return body; + } + + describe('PartitionFieldsValues', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await createMockJobs(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + describe('when model plot is enabled', () => { + it('should fetch anomalous only field values within the time range with an empty search term sorting by anomaly score', async () => { + const requestBody = { + jobId: 'fq_multi_1_ae', + criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }], + earliestMs: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latestMs: 1454976000000, // February 9, 2016 12:00:00 AM GMT, + searchTerm: {}, + fieldsConfig: { + partition_field: { + applyTimeRange: true, + anomalousOnly: true, + sort: { by: 'anomaly_score', order: 'desc' }, + }, + }, + }; + + const body = await runRequest(requestBody); + + expect(body.partition_field.name).to.eql('airline'); + expect(body.partition_field.values.length).to.eql(6); + expect(body.partition_field.values[0].value).to.eql('ACA'); + expect(body.partition_field.values[0].maxRecordScore).to.be.above(0); + expect(body.partition_field.values[1].value).to.eql('JBU'); + expect(body.partition_field.values[1].maxRecordScore).to.be.above(0); + expect(body.partition_field.values[2].value).to.eql('SWR'); + expect(body.partition_field.values[2].maxRecordScore).to.be.above(0); + expect(body.partition_field.values[3].value).to.eql('BAW'); + expect(body.partition_field.values[3].maxRecordScore).to.be.above(0); + expect(body.partition_field.values[4].value).to.eql('TRS'); + expect(body.partition_field.values[4].maxRecordScore).to.be.above(0); + expect(body.partition_field.values[5].value).to.eql('EGF'); + expect(body.partition_field.values[5].maxRecordScore).to.be.above(0); + }); + + it('should fetch all values withing the time range sorting by name', async () => { + const requestBody = { + jobId: 'fq_multi_1_ae', + criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }], + earliestMs: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latestMs: 1454976000000, // February 9, 2016 12:00:00 AM GMT, + searchTerm: {}, + fieldsConfig: { + partition_field: { + applyTimeRange: true, + anomalousOnly: false, + sort: { by: 'name', order: 'asc' }, + }, + }, + }; + + const body = await runRequest(requestBody); + + expect(body).to.eql({ + partition_field: { + name: 'airline', + values: [ + { value: 'AAL' }, + { value: 'ACA' }, + { value: 'AMX' }, + { value: 'ASA' }, + { value: 'AWE' }, + { value: 'BAW' }, + { value: 'DAL' }, + { value: 'EGF' }, + { value: 'FFT' }, + { value: 'JAL' }, + { value: 'JBU' }, + { value: 'JZA' }, + { value: 'KLM' }, + { value: 'NKS' }, + { value: 'SWA' }, + { value: 'SWR' }, + { value: 'TRS' }, + { value: 'UAL' }, + { value: 'VRD' }, + ], + }, + }); + }); + + it('should fetch anomalous only field value applying the search term', async () => { + const requestBody = { + jobId: 'fq_multi_1_ae', + criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }], + earliestMs: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latestMs: 1454976000000, // February 9, 2016 12:00:00 AM GMT, + searchTerm: { + partition_field: 'JB', + }, + fieldsConfig: { + partition_field: { + applyTimeRange: true, + anomalousOnly: true, + sort: { by: 'anomaly_score', order: 'asc' }, + }, + }, + }; + + const body = await runRequest(requestBody); + + expect(body.partition_field.name).to.eql('airline'); + expect(body.partition_field.values.length).to.eql(1); + expect(body.partition_field.values[0].value).to.eql('JBU'); + expect(body.partition_field.values[0].maxRecordScore).to.be.above(0); + }); + }); + + describe('when model plot is disabled', () => { + it('should fetch results within the time range', async () => { + const requestBody = { + jobId: 'fq_multi_2_ae', + criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }], + earliestMs: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latestMs: 1454976000000, // February 9, 2016 12:00:00 AM GMT, + searchTerm: {}, + fieldsConfig: { + partition_field: { + applyTimeRange: true, + anomalousOnly: false, + sort: { by: 'name', order: 'asc' }, + }, + }, + }; + + const body = await runRequest(requestBody); + expect(body.partition_field.values.length).to.eql(6); + }); + + it('should fetch results outside the time range', async () => { + const requestBody = { + jobId: 'fq_multi_2_ae', + criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }], + earliestMs: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latestMs: 1454976000000, // February 9, 2016 12:00:00 AM GMT, + searchTerm: {}, + fieldsConfig: { + partition_field: { + applyTimeRange: false, + anomalousOnly: false, + sort: { by: 'name', order: 'asc' }, + }, + }, + }; + + const body = await runRequest(requestBody); + expect(body.partition_field.values.length).to.eql(19); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/results/index.ts b/x-pack/test/api_integration/apis/ml/results/index.ts index 575435fa3a720..1580055df6d0e 100644 --- a/x-pack/test/api_integration/apis/ml/results/index.ts +++ b/x-pack/test/api_integration/apis/ml/results/index.ts @@ -14,5 +14,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get_stopped_partitions')); loadTestFile(require.resolve('./get_category_definition')); loadTestFile(require.resolve('./get_category_examples')); + loadTestFile(require.resolve('./max_anomaly_score')); + loadTestFile(require.resolve('./get_partition_fields_values')); }); } diff --git a/x-pack/test/api_integration/apis/ml/results/max_anomaly_score.ts b/x-pack/test/api_integration/apis/ml/results/max_anomaly_score.ts new file mode 100644 index 0000000000000..c706ee0ff96ee --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/results/max_anomaly_score.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Datafeed, Job } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + // @ts-expect-error not full interface + const JOB_CONFIG: Job = { + job_id: `fq_multi_1_ae`, + description: 'mean(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [ + { function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: false }, + }; + + // @ts-expect-error not full interface + const DATAFEED_CONFIG: Datafeed = { + datafeed_id: 'datafeed-fq_multi_1_ae', + indices: ['ft_farequote'], + job_id: 'fq_multi_1_ae', + query: { bool: { must: [{ match_all: {} }] } }, + }; + + async function createMockJobs() { + await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG); + } + + async function runRequest(requestBody: object) { + const { body, status } = await supertest + .post(`/api/ml/results/max_anomaly_score`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody); + return { body, status }; + } + + describe('MaxAnomalyScore', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await createMockJobs(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should fetch max anomaly score based on provided time range', async () => { + const requestBody = { + jobIds: [JOB_CONFIG.job_id], + earliestMs: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latestMs: 1454976000000, // February 9, 2016 12:00:00 AM GMT + }; + + const { body, status } = await runRequest(requestBody); + ml.api.assertResponseStatusCode(200, status, body); + expect(body).to.eql({ maxScore: 0 }); + }); + + it('should fetch max anomaly score from the entire range of data', async () => { + const requestBody = { + jobIds: [JOB_CONFIG.job_id], + }; + + const { body, status } = await runRequest(requestBody); + ml.api.assertResponseStatusCode(200, status, body); + expect(body.maxScore).to.be.above(50); + }); + it('should respond with an error when job with provided id does not exist', async () => { + const requestBody = { + jobIds: ['i_am_not_found'], + }; + + const { body, status } = await runRequest(requestBody); + ml.api.assertResponseStatusCode(404, status, body); + expect(body.message).to.eql('i_am_not_found missing'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/monitoring/_health/fixtures/response_empty.json b/x-pack/test/api_integration/apis/monitoring/_health/fixtures/response_empty.json new file mode 100644 index 0000000000000..d8bb99e30fc2b --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/_health/fixtures/response_empty.json @@ -0,0 +1,9 @@ +{ + "monitoredClusters": { + "clusters": {}, + "execution": { + "timedOut": false, + "errors": [] + } + } +} diff --git a/x-pack/test/api_integration/apis/monitoring/_health/fixtures/response_es_beats.js b/x-pack/test/api_integration/apis/monitoring/_health/fixtures/response_es_beats.js new file mode 100644 index 0000000000000..2b9ae96ba034b --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/_health/fixtures/response_es_beats.js @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; + +export const esBeatsResponse = (date = moment().format('YYYY.MM.DD')) => { + const esIndex = `.ds-.monitoring-es-8-mb-${date}-000001`; + const beatsIndex = `.ds-.monitoring-beats-8-mb-${date}-000001`; + + return { + monitoredClusters: { + clusters: { + tqiiSubfSgWrl68VJn4y2g: { + cluster: { + tqiiSubfSgWrl68VJn4y2g: { + index_summary: { + 'metricbeat-8': { + index: esIndex, + lastSeen: '2022-05-23T22:10:15.135Z', + }, + }, + index_recovery: { + 'metricbeat-8': { + index: esIndex, + lastSeen: '2022-05-23T22:10:13.584Z', + }, + }, + index: { + 'metricbeat-8': { + index: esIndex, + lastSeen: '2022-05-23T22:10:11.994Z', + }, + }, + cluster_stats: { + 'metricbeat-8': { + index: esIndex, + lastSeen: '2022-05-23T22:10:18.917Z', + }, + }, + }, + }, + elasticsearch: { + QR7smK2oReK_jWHtt5UOSQ: { + node_stats: { + 'metricbeat-8': { + index: esIndex, + lastSeen: '2022-05-23T22:10:14.268Z', + }, + }, + shard: { + 'metricbeat-8': { + index: esIndex, + lastSeen: '2022-05-23T22:09:48.321Z', + }, + }, + }, + }, + beats: { + 'metricbeat|726039e5-67fe-4e78-837f-072cf2ba0fe7': { + stats: { + 'metricbeat-8': { + index: beatsIndex, + lastSeen: '2022-05-23T22:17:10.622Z', + }, + }, + state: { + 'metricbeat-8': { + index: beatsIndex, + lastSeen: '2022-05-23T22:17:08.837Z', + }, + }, + }, + }, + }, + }, + execution: { + timedOut: false, + errors: [], + }, + }, + }; +}; diff --git a/x-pack/test/api_integration/apis/monitoring/_health/index.js b/x-pack/test/api_integration/apis/monitoring/_health/index.js new file mode 100644 index 0000000000000..cae6da2b3a13c --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/_health/index.js @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapValues } from 'lodash'; +import expect from '@kbn/expect'; + +import { getLifecycleMethods } from '../data_stream'; + +import emptyResponse from './fixtures/response_empty.json'; +import { esBeatsResponse } from './fixtures/response_es_beats'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + + describe('_health endpoint', () => { + const timeRange = { + min: '2022-05-23T22:00:00.000Z', + max: '2022-05-23T22:30:00.000Z', + }; + + describe('no data', () => { + it('returns an empty state when no data', async () => { + const { body } = await supertest + .get(`/api/monitoring/v1/_health?min=${timeRange.min}&max=${timeRange.max}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + delete body.settings; + expect(body).to.eql(emptyResponse); + }); + }); + + describe('with data', () => { + const archives = [ + 'x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_es_8', + 'x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_beats_8', + ]; + const { setup, tearDown } = getLifecycleMethods(getService); + + before('load archive', () => { + return Promise.all(archives.map(setup)); + }); + + after('unload archive', () => { + return tearDown(); + }); + + it('returns the state of the monitoring documents', async () => { + const { body } = await supertest + .get(`/api/monitoring/v1/_health?min=${timeRange.min}&max=${timeRange.max}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + delete body.settings; + expect(body).to.eql(esBeatsResponse()); + }); + + it('returns relevant settings', async () => { + const { + body: { settings }, + } = await supertest + .get(`/api/monitoring/v1/_health?min=${timeRange.min}&max=${timeRange.max}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + // we only test the structure of the settings and not the actual values + // to avoid coupling our tests with any underlying changes to default + // configuration + const settingsType = mapValues(settings, (value) => typeof value); + expect(settingsType).to.eql({ + ccs: 'boolean', + logsIndex: 'string', + metricbeatIndex: 'string', + hasRemoteClusterConfigured: 'boolean', + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/monitoring/apm/instances.js b/x-pack/test/api_integration/apis/monitoring/apm/instances.js index d915923991353..178a70f1d24a0 100644 --- a/x-pack/test/api_integration/apis/monitoring/apm/instances.js +++ b/x-pack/test/api_integration/apis/monitoring/apm/instances.js @@ -33,7 +33,7 @@ export default function ({ getService }) { .send({ timeRange }) .expect(200); - const expected = { + const expectedWithoutCgroup = { stats: { totalEvents: 18, apms: { @@ -67,10 +67,14 @@ export default function ({ getService }) { time_of_last_event: '2018-08-31T13:59:21.163Z', }, ], - cgroup: false, }; - expect(body).to.eql(expected); + // Due to the lack of `expect`s expressiveness this is an awkward way to + // tolate cgroup being false or true, which depends on the test execution + // environment. On cloud it is always true. + const { cgroup, ...bodyWithoutCgroup } = body; + expect(bodyWithoutCgroup).to.eql(expectedWithoutCgroup); + expect(cgroup).to.be.a('boolean'); }); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_beats_8/data.json.gz b/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_beats_8/data.json.gz new file mode 100644 index 0000000000000..9fbc5a31fe704 Binary files /dev/null and b/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_beats_8/data.json.gz differ diff --git a/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_es_8/data.json.gz b/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_es_8/data.json.gz new file mode 100644 index 0000000000000..5d47ccc83c1df Binary files /dev/null and b/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_es_8/data.json.gz differ diff --git a/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_kibana_8/data.json.gz b/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_kibana_8/data.json.gz new file mode 100644 index 0000000000000..55de0a6e215d7 Binary files /dev/null and b/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_kibana_8/data.json.gz differ diff --git a/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_logstash_standalone_8/data.json.gz b/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_logstash_standalone_8/data.json.gz new file mode 100644 index 0000000000000..740d9400460e2 Binary files /dev/null and b/x-pack/test/api_integration/apis/monitoring/es_archives/_health/monitoring_logstash_standalone_8/data.json.gz differ diff --git a/x-pack/test/api_integration/apis/monitoring/index.js b/x-pack/test/api_integration/apis/monitoring/index.js index 476911c904ea6..ebbf9c3af16e7 100644 --- a/x-pack/test/api_integration/apis/monitoring/index.js +++ b/x-pack/test/api_integration/apis/monitoring/index.js @@ -18,5 +18,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./standalone_cluster')); loadTestFile(require.resolve('./logs')); loadTestFile(require.resolve('./setup')); + loadTestFile(require.resolve('./_health')); }); } diff --git a/x-pack/test/api_integration/apis/security_solution/host_details.ts b/x-pack/test/api_integration/apis/security_solution/host_details.ts index 6a28dba22b0ab..6593636d21998 100644 --- a/x-pack/test/api_integration/apis/security_solution/host_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/host_details.ts @@ -198,7 +198,6 @@ export default function ({ getService }: FtrProviderContext) { architecture: ['armv7l'], id: ['b19a781f683541a7a25ee345133aa399'], ip: ['151.205.0.17'], - mac: [], name: ['raspberrypi'], os: { family: [''], @@ -207,16 +206,6 @@ export default function ({ getService }: FtrProviderContext) { version: ['9 (stretch)'], }, }, - cloud: { - instance: { - id: [], - }, - machine: { - type: [], - }, - provider: [], - region: [], - }, }, }; @@ -231,7 +220,6 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['filebeat-*'], - docValueFields: [], hostName: 'raspberrypi', inspect: false, }, diff --git a/x-pack/test/api_integration/apis/security_solution/hosts.ts b/x-pack/test/api_integration/apis/security_solution/hosts.ts index 653434f5fb25b..52e1b2eb375ba 100644 --- a/x-pack/test/api_integration/apis/security_solution/hosts.ts +++ b/x-pack/test/api_integration/apis/security_solution/hosts.ts @@ -49,7 +49,6 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*'], - docValueFields: [], sort: { field: HostsFields.lastSeen, direction: Direction.asc, @@ -84,7 +83,6 @@ export default function ({ getService }: FtrProviderContext) { direction: Direction.asc, }, defaultIndex: ['auditbeat-*'], - docValueFields: [], pagination: { activePage: 2, cursorStart: 1, @@ -112,7 +110,6 @@ export default function ({ getService }: FtrProviderContext) { from: FROM, }, defaultIndex: ['auditbeat-*'], - docValueFields: [], inspect: false, }, strategy: 'securitySolutionSearchStrategy', @@ -122,8 +119,6 @@ export default function ({ getService }: FtrProviderContext) { host: { architecture: ['x86_64'], id: [CURSOR_ID], - ip: [], - mac: [], name: ['zeek-sensor-san-francisco'], os: { family: ['debian'], @@ -136,22 +131,18 @@ export default function ({ getService }: FtrProviderContext) { instance: { id: ['132972452'], }, - machine: { - type: [], - }, provider: ['digitalocean'], region: ['sfo2'], }, }); }); - it('Make sure that we get First Seen for a Host without docValueFields', async () => { + it('Make sure that we get First Seen for a Host', async () => { const firstLastSeenHost = await bsearch.send({ supertest, options: { factoryQueryType: HostsQueries.firstOrLastSeen, defaultIndex: ['auditbeat-*'], - docValueFields: [], hostName: 'zeek-sensor-san-francisco', order: 'asc', }, @@ -160,13 +151,12 @@ export default function ({ getService }: FtrProviderContext) { expect(firstLastSeenHost.firstSeen).to.eql('2019-02-19T19:36:23.561Z'); }); - it('Make sure that we get Last Seen for a Host without docValueFields', async () => { + it('Make sure that we get Last Seen for a Host', async () => { const firstLastSeenHost = await bsearch.send({ supertest, options: { factoryQueryType: HostsQueries.firstOrLastSeen, defaultIndex: ['auditbeat-*'], - docValueFields: [], hostName: 'zeek-sensor-san-francisco', order: 'desc', }, @@ -174,35 +164,5 @@ export default function ({ getService }: FtrProviderContext) { }); expect(firstLastSeenHost.lastSeen).to.eql('2019-02-19T20:42:33.561Z'); }); - - it('Make sure that we get First Seen for a Host with docValueFields', async () => { - const firstLastSeenHost = await bsearch.send({ - supertest, - options: { - factoryQueryType: HostsQueries.firstOrLastSeen, - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - docValueFields: [{ field: '@timestamp', format: 'epoch_millis' }], - hostName: 'zeek-sensor-san-francisco', - order: 'asc', - }, - strategy: 'securitySolutionSearchStrategy', - }); - expect(firstLastSeenHost.firstSeen).to.eql(new Date('2019-02-19T19:36:23.561Z').valueOf()); - }); - - it('Make sure that we get Last Seen for a Host with docValueFields', async () => { - const firstLastSeenHost = await bsearch.send({ - supertest, - options: { - factoryQueryType: HostsQueries.firstOrLastSeen, - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - docValueFields: [{ field: '@timestamp', format: 'epoch_millis' }], - hostName: 'zeek-sensor-san-francisco', - order: 'desc', - }, - strategy: 'securitySolutionSearchStrategy', - }); - expect(firstLastSeenHost.lastSeen).to.eql(new Date('2019-02-19T20:42:33.561Z').valueOf()); - }); }); } diff --git a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts index bf30a39e80c89..0278852b80672 100644 --- a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts +++ b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts @@ -128,7 +128,6 @@ export default function ({ getService }: FtrProviderContext) { _id: 'HCFxB2kBR346wHgnL4ik', instances: 1, process: { - args: [], name: ['kworker/u2:0'], }, user: { diff --git a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts index 218ac55e73c61..e0335d86ea37c 100644 --- a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts @@ -11,6 +11,7 @@ import { apm, timerange } from '@elastic/apm-synthtrace'; import { ServiceAnomalyTimeseries } from '@kbn/apm-plugin/common/anomaly_detection/service_anomaly_timeseries'; import { ApmMlDetectorType } from '@kbn/apm-plugin/common/anomaly_detection/apm_ml_detectors'; import { Environment } from '@kbn/apm-plugin/common/environment_rt'; +import { last } from 'lodash'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { ApmApiError } from '../../common/apm_api_supertest'; import { createAndRunApmMlJob } from '../../common/utils/create_and_run_apm_ml_job'; @@ -202,6 +203,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let latencySeries: ServiceAnomalyTimeseries | undefined; let throughputSeries: ServiceAnomalyTimeseries | undefined; let failureRateSeries: ServiceAnomalyTimeseries | undefined; + const endTimeMs = new Date(end).getTime(); before(async () => { allAnomalyTimeseries = ( @@ -233,6 +235,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); }); + it('returns model plots with bounds for x range within start and end', () => { + expect(allAnomalyTimeseries.length).to.eql(3); + + expect( + allAnomalyTimeseries.every((spec) => + spec.bounds.every( + (bound) => bound.x >= new Date(start).getTime() && bound.x <= endTimeMs + ) + ) + ); + }); + it('returns model plots with latest bucket matching the end time', () => { + expect(allAnomalyTimeseries.every((spec) => last(spec.bounds)?.x === endTimeMs)); + }); + it('returns the correct metadata', () => { function omitTimeseriesData(series: ServiceAnomalyTimeseries | undefined) { return series ? omit(series, 'anomalies', 'bounds') : undefined; diff --git a/x-pack/test/apm_api_integration/tests/csm/js_errors.spec.ts b/x-pack/test/apm_api_integration/tests/csm/js_errors.spec.ts deleted file mode 100644 index b23f365348328..0000000000000 --- a/x-pack/test/apm_api_integration/tests/csm/js_errors.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 '../../common/ftr_provider_context'; - -export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmReadUser'); - - registry.when('CSM JS errors with data', { config: 'trial', archives: [] }, () => { - it('returns no js errors', async () => { - const response = await supertest.get('/internal/apm/ux/js-errors').query({ - pageSize: 5, - pageIndex: 0, - start: '2020-09-07T20:35:54.654Z', - end: '2020-09-14T20:35:54.654Z', - uiFilters: '{"serviceName":["elastic-co-rum-test"]}', - }); - - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(` - Object { - "totalErrorGroups": 0, - "totalErrorPages": 0, - "totalErrors": 0, - } - `); - }); - }); - - registry.when( - 'CSM JS errors without data', - { config: 'trial', archives: ['8.0.0', 'rum_test_data'] }, - () => { - it('returns js errors', async () => { - const response = await supertest.get('/internal/apm/ux/js-errors').query({ - start: '2021-01-18T12:20:17.202Z', - end: '2021-01-18T12:25:17.203Z', - uiFilters: '{"environment":"ENVIRONMENT_ALL","serviceName":["elastic-co-frontend"]}', - pageSize: 5, - pageIndex: 0, - }); - - expect(response.status).to.be(200); - - expectSnapshot(response.body).toMatchInline(` - Object { - "items": Array [ - Object { - "count": 5, - "errorGroupId": "de32dc81e2ee5165cbff20046c080a27", - "errorMessage": "SyntaxError: Document.querySelector: '' is not a valid selector", - }, - Object { - "count": 2, - "errorGroupId": "34d83587e17711a7c257ffb080ddb1c6", - "errorMessage": "Uncaught SyntaxError: Failed to execute 'querySelector' on 'Document': The provided selector is empty.", - }, - Object { - "count": 43, - "errorGroupId": "3dd5604267b928139d958706f09f7e09", - "errorMessage": "Script error.", - }, - Object { - "count": 1, - "errorGroupId": "cd3a2b01017ff7bcce70479644f28318", - "errorMessage": "Unhandled promise rejection: TypeError: can't convert undefined to object", - }, - Object { - "count": 3, - "errorGroupId": "23539422cf714db071aba087dd041859", - "errorMessage": "Unable to get property 'left' of undefined or null reference", - }, - ], - "totalErrorGroups": 6, - "totalErrorPages": 120, - "totalErrors": 2846, - } - `); - }); - } - ); -} 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 739f8e5ec0892..691d132aa1ede 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 9de57a1b7abe2..d94cea06e5713 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 6513fe25b28e9..723e2330bdd93 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 3ea6fac3772ed..0df628f93b28b 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 361358dc40604..ccf22279ec197 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 4ac0573428445..f78816dd7ba1d 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 d3c3176f4649f..983983f5d3b3e 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 c4e46675e7549..b48543d3699f6 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 630628a13b6b9..41074cd252735 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 7e5abeb7edc2f..e43c41809eb3a 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 2569d035fabb5..5bc9fd400de77 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 ea3766b733cdc..9eba39d0a8545 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 048700993087d..ebffac50e131a 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 286c0d7b6925d..395fa81dcb968 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 0f268e3288c82..1eccb0dba8759 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 573b96d71af4a..b1873d912890d 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 f61e8698c1191..c681718d50cb5 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 161075616925c..a3e7a8f58e24a 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 66fb3f4343e58..9ce6119060fe1 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 199e53ebd1bb5..4b97d30ecf1f5 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 bfb266e6f6c3a..09cbc2b9ad18b 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 405323005e64e..c68437968e0da 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 0ca47597e7b6b..b08b6f21390d4 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 084e05921fb47..e984cab68f9dd 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 13d2cc6070727..4a8ffe56d35eb 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/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index ba13772b10253..12ae8a180f255 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -574,6 +574,76 @@ export default function (providerContext: FtrProviderContext) { 'Cannot update name in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.' ); }); + + it('should return a 200 if updating monitoring_enabled on a policy', async () => { + const fetchPackageList = async () => { + const response = await supertest + .get('/api/fleet/epm/packages') + .set('kbn-xsrf', 'xxx') + .expect(200); + return response.body; + }; + + const { + body: { item: originalPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test_policy', + description: 'Initial description', + namespace: 'default', + }) + .expect(200); + + // uninstall the elastic_agent and verify that is installed after the policy update + await supertest + .delete(`/api/fleet/epm/packages/elastic_agent/1.3.3`) + .set('kbn-xsrf', 'xxxx'); + + const listResponse = await fetchPackageList(); + const installedPackages = listResponse.items.filter( + (item: any) => item.status === 'installed' + ); + + expect(installedPackages.length).to.be(0); + + agentPolicyId = originalPolicy.id; + const { + body: { item: updatedPolicy }, + } = await supertest + .put(`/api/fleet/agent_policies/${agentPolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test_policy_with_monitoring', + description: 'Updated description', + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + }) + .expect(200); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { id, updated_at, ...newPolicy } = updatedPolicy; + createdPolicyIds.push(updatedPolicy.id); + + expect(newPolicy).to.eql({ + status: 'active', + name: 'Test_policy_with_monitoring', + description: 'Updated description', + namespace: 'default', + is_managed: false, + revision: 2, + updated_by: 'elastic', + package_policies: [], + monitoring_enabled: ['logs', 'metrics'], + }); + + const listResponseAfterUpdate = await fetchPackageList(); + + const installedPackagesAfterUpdate = listResponseAfterUpdate.items + .filter((item: any) => item.status === 'installed') + .map((item: any) => item.name); + expect(installedPackagesAfterUpdate).to.contain('elastic_agent'); + }); }); describe('POST /api/fleet/agent_policies/delete', () => { diff --git a/x-pack/test/fleet_api_integration/apis/agents/current_upgrades.ts b/x-pack/test/fleet_api_integration/apis/agents/current_upgrades.ts index 8d060666cd1ee..9d7e7e655c7a1 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/current_upgrades.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/current_upgrades.ts @@ -150,13 +150,26 @@ export default function (providerContext: FtrProviderContext) { }, }, }); + + // Action 6 1 agent with not start time + await es.index({ + refresh: 'wait_for', + index: AGENT_ACTIONS_INDEX, + document: { + type: 'UPGRADE', + action_id: 'action6', + agents: ['agent1'], + expiration: moment().add(1, 'day').toISOString(), + }, + }); }); it('should respond 200 and the current upgrades', async () => { const res = await supertest.get(`/api/fleet/agents/current_upgrades`).expect(200); const actionIds = res.body.items.map((item: any) => item.actionId); - expect(actionIds).length(2); + expect(actionIds).length(3); expect(actionIds).contain('action1'); expect(actionIds).contain('action2'); + expect(actionIds).contain('action6'); }); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index 28b68609ce15e..a952d24129894 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -163,7 +163,7 @@ export default function (providerContext: FtrProviderContext) { .send(buf) .expect(400); expect(res.error.text).to.equal( - '{"statusCode":400,"error":"Bad Request","message":"Invalid top-level package manifest: one or more fields missing of name, version, description, title, format_version, release, owner"}' + '{"statusCode":400,"error":"Bad Request","message":"Invalid top-level package manifest: one or more fields missing of name, version, description, title, format_version, owner"}' ); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 0d06a1ca9e0f7..44d609c5d492e 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -12,6 +12,12 @@ import { FtrProviderContext } from '../../../api_integration/ftr_provider_contex import { skipIfNoDockerRegistry } from '../../helpers'; import { setupFleetAndAgents } from '../agents/services'; +function checkErrorWithResponseDataOrThrow(err: any) { + if (!err?.response?.data) { + throw err; + } +} + export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const kibanaServer = getService('kibanaServer'); @@ -57,15 +63,18 @@ export default function (providerContext: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/132333 - describe.skip('uninstalls all assets when uninstalling a package', async () => { - before(async () => { + describe('uninstalls all assets when uninstalling a package', async () => { + // these tests ensure that uninstall works properly so make sure that the package gets installed and uninstalled + // and then we'll test that not artifacts are left behind. + before(() => { if (!server.enabled) return; - // these tests ensure that uninstall works properly so make sure that the package gets installed and uninstalled - // and then we'll test that not artifacts are left behind. - await installPackage(pkgName, pkgVersion); - await uninstallPackage(pkgName, pkgVersion); + return installPackage(pkgName, pkgVersion); + }); + before(() => { + if (!server.enabled) return; + return uninstallPackage(pkgName, pkgVersion); }); + it('should have uninstalled the index templates', async function () { const resLogsTemplate = await es.transport.request( { @@ -199,6 +208,7 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_dashboard', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); resDashboard = err; } expect(resDashboard.response.data.statusCode).equal(404); @@ -209,6 +219,7 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_dashboard2', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); resDashboard2 = err; } expect(resDashboard2.response.data.statusCode).equal(404); @@ -219,6 +230,7 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_visualization', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); resVis = err; } expect(resVis.response.data.statusCode).equal(404); @@ -229,6 +241,7 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_search', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); resSearch = err; } expect(resSearch.response.data.statusCode).equal(404); @@ -239,6 +252,7 @@ export default function (providerContext: FtrProviderContext) { id: 'test-*', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); resIndexPattern = err; } expect(resIndexPattern.response.data.statusCode).equal(404); @@ -249,6 +263,7 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_osquery_pack_asset', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); resOsqueryPackAsset = err; } expect(resOsqueryPackAsset.response.data.statusCode).equal(404); @@ -259,6 +274,7 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_osquery_saved_query', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); resOsquerySavedQuery = err; } expect(resOsquerySavedQuery.response.data.statusCode).equal(404); @@ -271,6 +287,7 @@ export default function (providerContext: FtrProviderContext) { id: 'all_assets', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); res = err; } expect(res.response.data.statusCode).equal(404); @@ -482,6 +499,7 @@ const expectAssetsInstalled = ({ id: 'invalid', }); } catch (err) { + checkErrorWithResponseDataOrThrow(err); resInvalidTypeIndexPattern = err; } expect(resInvalidTypeIndexPattern.response.data.statusCode).equal(404); 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 6d79a1c0a85c4..00d964cb64e7c 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/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/agent/stream/log.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/agent/stream/log.yml.hbs new file mode 100644 index 0000000000000..0146a6e9c63a6 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/agent/stream/log.yml.hbs @@ -0,0 +1,10 @@ +paths: +{{#each paths as |path i|}} + - {{path}} +{{/each}} +exclude_files: [".gz$"] +processors: + - add_fields: + target: '' + fields: + ecs.version: 1.5.0 \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/elasticsearch/ingest_pipeline/default.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/elasticsearch/ingest_pipeline/default.yml new file mode 100644 index 0000000000000..9e0d5272bed9b --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/elasticsearch/ingest_pipeline/default.yml @@ -0,0 +1,101 @@ +--- +description: "Pipeline for parsing Apache HTTP Server access logs. Requires the geoip and user_agent plugins." + +processors: +- grok: + field: message + patterns: + - '%{IPORHOST:destination.domain} %{IPORHOST:source.ip} - %{DATA:user.name} \[%{HTTPDATE:apache.access.time}\] + "(?:%{WORD:http.request.method} %{DATA:url.original} HTTP/%{NUMBER:http.version}|-)?" + %{NUMBER:http.response.status_code:long} (?:%{NUMBER:http.response.body.bytes:long}|-)( + "%{DATA:http.request.referrer}")?( "%{DATA:user_agent.original}")?' + - '%{IPORHOST:source.address} - %{DATA:user.name} \[%{HTTPDATE:apache.access.time}\] + "(?:%{WORD:http.request.method} %{DATA:url.original} HTTP/%{NUMBER:http.version}|-)?" + %{NUMBER:http.response.status_code:long} (?:%{NUMBER:http.response.body.bytes:long}|-)( + "%{DATA:http.request.referrer}")?( "%{DATA:user_agent.original}")?' + - '%{IPORHOST:source.address} - %{DATA:user.name} \[%{HTTPDATE:apache.access.time}\] + "-" %{NUMBER:http.response.status_code:long} -' + - \[%{HTTPDATE:apache.access.time}\] %{IPORHOST:source.address} %{DATA:apache.access.ssl.protocol} + %{DATA:apache.access.ssl.cipher} "%{WORD:http.request.method} %{DATA:url.original} + HTTP/%{NUMBER:http.version}" (-|%{NUMBER:http.response.body.bytes:long}) + ignore_missing: true +- remove: + field: message +- set: + field: event.kind + value: event +- set: + field: event.category + value: web +- set: + field: event.outcome + value: success + if: "ctx?.http?.response?.status_code != null && ctx.http.response.status_code < 400" +- set: + field: event.outcome + value: failure + if: "ctx?.http?.response?.status_code != null && ctx.http.response.status_code > 399" +- grok: + field: source.address + ignore_missing: true + patterns: + - ^(%{IP:source.ip}|%{HOSTNAME:source.domain})$ +- rename: + field: '@timestamp' + target_field: event.created +- date: + field: apache.access.time + target_field: '@timestamp' + formats: + - dd/MMM/yyyy:H:m:s Z + ignore_failure: true +- remove: + field: apache.access.time + ignore_failure: true +- user_agent: + field: user_agent.original + ignore_failure: true +- geoip: + field: source.ip + target_field: source.geo + ignore_missing: true +- geoip: + database_file: GeoLite2-ASN.mmdb + field: source.ip + target_field: source.as + properties: + - asn + - organization_name + ignore_missing: true +- rename: + field: source.as.asn + target_field: source.as.number + ignore_missing: true +- rename: + field: source.as.organization_name + target_field: source.as.organization.name + ignore_missing: true +- set: + field: tls.cipher + value: '{{apache.access.ssl.cipher}}' + if: ctx?.apache?.access?.ssl?.cipher != null + +- script: + lang: painless + if: ctx?.apache?.access?.ssl?.protocol != null + source: >- + def parts = ctx.apache.access.ssl.protocol.toLowerCase().splitOnToken("v"); + if (parts.length != 2) { + return; + } + if (parts[1].contains(".")) { + ctx.tls.version = parts[1]; + } else { + ctx.tls.version = parts[1] + ".0"; + } + ctx.tls.version_protocol = parts[0]; + +on_failure: +- set: + field: error.message + value: '{{ _ingest.on_failure_message }}' diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/base-fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/base-fields.yml new file mode 100644 index 0000000000000..5dab30cf9768c --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/base-fields.yml @@ -0,0 +1,13 @@ +- name: data_stream.type + type: constant_keyword + description: Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: Data stream namespace. +- name: '@timestamp' + type: date + description: Event timestamp. + diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/ecs.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/ecs.yml new file mode 100644 index 0000000000000..710a5a1ad6540 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/ecs.yml @@ -0,0 +1,187 @@ +- name: message + level: core + type: text + description: |- + For log events the message field contains the log message, optimized for viewing in a log viewer. + For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. + If multiple messages exist, they can be combined into one message. +- name: http + title: HTTP + group: 2 + type: group + fields: + - name: request.method + level: extended + type: keyword + description: |- + HTTP request method. + Prior to ECS 1.6.0 the following guidance was provided: + "The field value must be normalized to lowercase for querying." + As of ECS 1.6.0, the guidance is deprecated because the original case of the method may be useful in anomaly detection. Original case will be mandated in ECS 2.0.0 + ignore_above: 1024 + - name: request.referrer + level: extended + type: keyword + description: Referrer for this HTTP request. + ignore_above: 1024 + - name: response.body.bytes + level: extended + type: long + format: bytes + description: Size in bytes of the response body. + - name: response.status_code + level: extended + type: long + format: string + description: HTTP response status code. + - name: version + level: extended + type: keyword + description: HTTP version. + ignore_above: 1024 +- name: log + title: Log + group: 2 + type: group + fields: + - name: level + level: core + type: keyword + description: |- + Original log level of the log event. + If the source of the event provides a log level or textual severity, this is the one that goes in `log.level`. If your source doesn't specify one, you may put your event transport's severity here (e.g. Syslog severity). + Some examples are `warn`, `err`, `i`, `informational`. + ignore_above: 1024 +- name: process + title: Process + group: 2 + type: group + fields: + - name: pid + level: core + type: long + format: string + description: Process id. + - name: thread.id + level: extended + type: long + format: string + description: Thread ID. +- name: source + title: Source + group: 2 + type: group + fields: + - name: address + level: extended + type: keyword + description: |- + Some event source addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. + Then it should be duplicated to `.ip` or `.domain`, depending on which one it is. + ignore_above: 1024 + - name: geo.city_name + level: core + type: keyword + description: City name. + ignore_above: 1024 + - name: geo.continent_name + level: core + type: keyword + description: Name of the continent. + ignore_above: 1024 + - name: geo.country_iso_code + level: core + type: keyword + description: Country ISO code. + ignore_above: 1024 + - name: geo.location + level: core + type: geo_point + description: Longitude and latitude. + - name: geo.region_iso_code + level: core + type: keyword + description: Region ISO code. + ignore_above: 1024 + - name: geo.region_name + level: core + type: keyword + description: Region name. + ignore_above: 1024 +- name: url + title: URL + group: 2 + type: group + fields: + - name: original + level: extended + type: keyword + description: |- + Unmodified original url as seen in the event source. + Note that in network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. + This field is meant to represent the URL as it was observed, complete or not. + ignore_above: 1024 + multi_fields: + - name: text + type: text + norms: false + default_field: false +- name: user + title: User + group: 2 + type: group + fields: + - name: name + level: core + type: keyword + description: Short name or login of the user. + ignore_above: 1024 + multi_fields: + - name: text + type: text + norms: false + default_field: false +- name: user_agent + title: User agent + group: 2 + type: group + fields: + - name: device.name + level: extended + type: keyword + description: Name of the device. + ignore_above: 1024 + - name: name + level: extended + type: keyword + description: Name of the user agent. + ignore_above: 1024 + - name: original + level: extended + type: keyword + description: Unparsed user_agent string. + ignore_above: 1024 + multi_fields: + - name: text + type: text + norms: false + - name: os.name + level: extended + type: keyword + description: Operating system name, without the version. + ignore_above: 1024 + multi_fields: + - name: text + type: text + norms: false + default_field: false + - name: os.version + level: extended + type: keyword + ignore_above: 1024 + description: Operating system version as a raw string. + - name: version + level: extended + type: keyword + ignore_above: 1024 + description: Version of the user agent. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/fields.yml new file mode 100644 index 0000000000000..b39dc4e598f8b --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/fields/fields.yml @@ -0,0 +1,11 @@ +- name: apache.access + type: group + fields: + - name: ssl.protocol + type: keyword + description: | + SSL protocol version. + - name: ssl.cipher + type: keyword + description: | + SSL cipher name. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/manifest.yml new file mode 100644 index 0000000000000..e541241c979ba --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/access/manifest.yml @@ -0,0 +1,19 @@ +title: Apache access logs +release: experimental +type: logs +streams: +- input: logfile + vars: + - name: paths + type: text + title: Paths + multi: true + required: true + show_user: true + default: + - /var/log/apache2/access.log* + - /var/log/apache2/other_vhosts_access.log* + - /var/log/httpd/access_log* + template_path: log.yml.hbs + title: Apache access logs + description: Collect Apache access logs diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/agent/stream/log.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/agent/stream/log.yml.hbs new file mode 100644 index 0000000000000..9a26f86f59763 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/agent/stream/log.yml.hbs @@ -0,0 +1,11 @@ +paths: +{{#each paths as |path i|}} + - {{path}} +{{/each}} +exclude_files: [".gz$"] +processors: + - add_locale: ~ + - add_fields: + target: '' + fields: + ecs.version: 1.5.0 \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/elasticsearch/ingest_pipeline/default.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/elasticsearch/ingest_pipeline/default.yml new file mode 100644 index 0000000000000..a39c890f69836 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/elasticsearch/ingest_pipeline/default.yml @@ -0,0 +1,86 @@ +--- +description: Pipeline for parsing apache error logs +processors: +- grok: + field: message + patterns: + - \[%{APACHE_TIME:apache.error.timestamp}\] \[%{LOGLEVEL:log.level}\]( \[client + %{IPORHOST:source.address}(:%{POSINT:source.port})?\])? %{GREEDYDATA:message} + - \[%{APACHE_TIME:apache.error.timestamp}\] \[%{DATA:apache.error.module}:%{LOGLEVEL:log.level}\] + \[pid %{NUMBER:process.pid:long}(:tid %{NUMBER:process.thread.id:long})?\]( + \[client %{IPORHOST:source.address}(:%{POSINT:source.port})?\])? %{GREEDYDATA:message} + pattern_definitions: + APACHE_TIME: '%{DAY} %{MONTH} %{MONTHDAY} %{TIME} %{YEAR}' + ignore_missing: true +- date: + if: ctx.event.timezone == null + field: apache.error.timestamp + target_field: '@timestamp' + formats: + - EEE MMM dd H:m:s yyyy + - EEE MMM dd H:m:s.SSSSSS yyyy + on_failure: + - append: + field: error.message + value: '{{ _ingest.on_failure_message }}' +- date: + if: ctx.event.timezone != null + field: apache.error.timestamp + target_field: '@timestamp' + formats: + - EEE MMM dd H:m:s yyyy + - EEE MMM dd H:m:s.SSSSSS yyyy + timezone: '{{ event.timezone }}' + on_failure: + - append: + field: error.message + value: '{{ _ingest.on_failure_message }}' +- remove: + field: apache.error.timestamp + ignore_failure: true +- set: + field: event.kind + value: event +- set: + field: event.category + value: web +- script: + if: "ctx?.log?.level != null" + lang: painless + source: >- + def err_levels = ["emerg", "alert", "crit", "error", "warn"]; + if (err_levels.contains(ctx.log.level)) { + ctx.event.type = "error"; + } else { + ctx.event.type = "info"; + } + +- grok: + field: source.address + ignore_missing: true + patterns: + - ^(%{IP:source.ip}|%{HOSTNAME:source.domain})$ +- geoip: + field: source.ip + target_field: source.geo + ignore_missing: true +- geoip: + database_file: GeoLite2-ASN.mmdb + field: source.ip + target_field: source.as + properties: + - asn + - organization_name + ignore_missing: true +- rename: + field: source.as.asn + target_field: source.as.number + ignore_missing: true +- rename: + field: source.as.organization_name + target_field: source.as.organization.name + ignore_missing: true +on_failure: +- set: + field: error.message + value: '{{ _ingest.on_failure_message }}' diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/base-fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/base-fields.yml new file mode 100644 index 0000000000000..5dab30cf9768c --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/base-fields.yml @@ -0,0 +1,13 @@ +- name: data_stream.type + type: constant_keyword + description: Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: Data stream namespace. +- name: '@timestamp' + type: date + description: Event timestamp. + diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/ecs.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/ecs.yml new file mode 100644 index 0000000000000..a96aabb70cbb6 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/ecs.yml @@ -0,0 +1,177 @@ +- name: message + level: core + type: text + description: |- + For log events the message field contains the log message, optimized for viewing in a log viewer. + For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. + If multiple messages exist, they can be combined into one message. +- name: http + title: HTTP + group: 2 + type: group + fields: + - name: request.method + level: extended + type: keyword + description: |- + HTTP request method. + Prior to ECS 1.6.0 the following guidance was provided: + "The field value must be normalized to lowercase for querying." + As of ECS 1.6.0, the guidance is deprecated because the original case of the method may be useful in anomaly detection. Original case will be mandated in ECS 2.0.0 + ignore_above: 1024 + - name: request.referrer + level: extended + type: keyword + description: Referrer for this HTTP request. + ignore_above: 1024 + - name: response.body.bytes + level: extended + type: long + format: bytes + description: Size in bytes of the response body. + - name: response.status_code + level: extended + type: long + format: string + description: HTTP response status code. + - name: version + level: extended + type: keyword + description: HTTP version. + ignore_above: 1024 +- name: log + title: Log + group: 2 + type: group + fields: + - name: level + level: core + type: keyword + description: |- + Original log level of the log event. + If the source of the event provides a log level or textual severity, this is the one that goes in `log.level`. If your source doesn't specify one, you may put your event transport's severity here (e.g. Syslog severity). + Some examples are `warn`, `err`, `i`, `informational`. + ignore_above: 1024 +- name: process + title: Process + group: 2 + type: group + fields: + - name: pid + level: core + type: long + format: string + description: Process id. + - name: thread.id + level: extended + type: long + format: string + description: Thread ID. +- name: source + title: Source + group: 2 + type: group + fields: + - name: address + level: extended + type: keyword + description: |- + Some event source addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. + Then it should be duplicated to `.ip` or `.domain`, depending on which one it is. + ignore_above: 1024 + - name: geo.city_name + level: core + type: keyword + description: City name. + ignore_above: 1024 + - name: geo.continent_name + level: core + type: keyword + description: Name of the continent. + ignore_above: 1024 + - name: geo.country_iso_code + level: core + type: keyword + description: Country ISO code. + ignore_above: 1024 + - name: geo.location + level: core + type: geo_point + description: Longitude and latitude. + - name: geo.region_iso_code + level: core + type: keyword + description: Region ISO code. + ignore_above: 1024 + - name: geo.region_name + level: core + type: keyword + description: Region name. + ignore_above: 1024 +- name: url + title: URL + group: 2 + type: group + fields: + - name: original + level: extended + type: keyword + description: |- + Unmodified original url as seen in the event source. + Note that in network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. + This field is meant to represent the URL as it was observed, complete or not. + ignore_above: 1024 + multi_fields: + - name: text + type: text + norms: false + default_field: false +- name: user + title: User + group: 2 + type: group + fields: + - name: name + level: core + type: keyword + description: Short name or login of the user. + ignore_above: 1024 + multi_fields: + - name: text + type: text + norms: false + default_field: false +- name: user_agent + title: User agent + group: 2 + type: group + fields: + - name: device.name + level: extended + type: keyword + description: Name of the device. + ignore_above: 1024 + - name: name + level: extended + type: keyword + description: Name of the user agent. + ignore_above: 1024 + - name: original + level: extended + type: keyword + description: Unparsed user_agent string. + ignore_above: 1024 + multi_fields: + - name: text + type: text + norms: false + - name: os.name + level: extended + type: keyword + description: Operating system name, without the version. + ignore_above: 1024 + multi_fields: + - name: text + type: text + norms: false + default_field: false diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/fields.yml new file mode 100644 index 0000000000000..a1c6712cadc64 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/fields/fields.yml @@ -0,0 +1,7 @@ +- name: apache.error + type: group + fields: + - name: module + type: keyword + description: | + The module producing the logged message. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/manifest.yml new file mode 100644 index 0000000000000..96b15e7b2a230 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/error/manifest.yml @@ -0,0 +1,18 @@ +title: Apache error logs +release: experimental +type: logs +streams: +- input: logfile + vars: + - name: paths + type: text + title: Paths + multi: true + required: true + show_user: true + default: + - /var/log/apache2/error.log* + - /var/log/httpd/error_log* + template_path: log.yml.hbs + title: Apache error logs + description: Collect Apache error logs diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/agent/stream/stream.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/agent/stream/stream.yml.hbs new file mode 100644 index 0000000000000..9c7975244ba69 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/agent/stream/stream.yml.hbs @@ -0,0 +1,9 @@ +metricsets: ["status"] +hosts: +{{#each hosts}} + - {{this}} +{{/each}} +period: {{period}} +{{#if server_status_path}} +server_status_path: {{server_status_path}} +{{/if}} diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/fields/base-fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/fields/base-fields.yml new file mode 100644 index 0000000000000..5dab30cf9768c --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/fields/base-fields.yml @@ -0,0 +1,13 @@ +- name: data_stream.type + type: constant_keyword + description: Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: Data stream namespace. +- name: '@timestamp' + type: date + description: Event timestamp. + diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/fields/fields.yml new file mode 100644 index 0000000000000..473c7d97dfb74 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/fields/fields.yml @@ -0,0 +1,154 @@ +- name: apache.status + type: group + fields: + - name: hostname + type: keyword + description: | + Apache hostname. + - name: total_accesses + type: long + description: | + Total number of access requests. + - name: total_kbytes + type: long + description: | + Total number of kilobytes served. + - name: requests_per_sec + type: scaled_float + description: | + Requests per second. + - name: bytes_per_sec + type: scaled_float + description: | + Bytes per second. + - name: bytes_per_request + type: scaled_float + description: | + Bytes per request. + - name: workers.busy + type: long + description: | + Number of busy workers. + - name: workers.idle + type: long + description: | + Number of idle workers. + - name: uptime + type: group + fields: + - name: server_uptime + type: long + description: | + Server uptime in seconds. + - name: uptime + type: long + description: | + Server uptime. + - name: cpu + type: group + fields: + - name: load + type: scaled_float + description: | + CPU Load. + - name: user + type: scaled_float + description: | + CPU user load. + - name: system + type: scaled_float + description: | + System cpu. + - name: children_user + type: scaled_float + description: | + CPU of children user. + - name: children_system + type: scaled_float + description: | + CPU of children system. + - name: connections + type: group + fields: + - name: total + type: long + description: | + Total connections. + - name: async.writing + type: long + description: | + Async connection writing. + - name: async.keep_alive + type: long + description: | + Async keeped alive connections. + - name: async.closing + type: long + description: | + Async closed connections. + - name: load + type: group + fields: + - name: "1" + type: scaled_float + description: | + Load average for the last minute. + - name: "5" + type: scaled_float + description: | + Load average for the last 5 minutes. + - name: "15" + type: scaled_float + description: | + Load average for the last 15 minutes. + - name: scoreboard + type: group + fields: + - name: starting_up + type: long + description: | + Starting up. + - name: reading_request + type: long + description: | + Reading requests. + - name: sending_reply + type: long + description: | + Sending Reply. + - name: keepalive + type: long + description: | + Keep alive. + - name: dns_lookup + type: long + description: | + Dns Lookups. + - name: closing_connection + type: long + description: | + Closing connections. + - name: logging + type: long + description: | + Logging + - name: gracefully_finishing + type: long + description: | + Gracefully finishing. + - name: idle_cleanup + type: long + description: | + Idle cleanups. + - name: open_slot + type: long + description: | + Open slots. + - name: waiting_for_connection + type: long + description: | + Waiting for connections. + - name: total + type: long + description: | + Total. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/manifest.yml new file mode 100644 index 0000000000000..e86342a6286d3 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/manifest.yml @@ -0,0 +1,22 @@ +title: Apache status metrics +release: experimental +type: metrics +streams: +- input: apache/metrics + vars: + - name: period + type: text + title: Period + multi: false + required: true + show_user: true + default: 10s + - name: server_status_path + type: text + title: Server Status Path + multi: false + required: true + show_user: false + default: /server-status + title: Apache status metrics + description: Collect Apache status metrics diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/sample_event.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/sample_event.json new file mode 100644 index 0000000000000..c6fcf9584e5e0 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/dataset/status/sample_event.json @@ -0,0 +1,94 @@ +{ + "@timestamp": "2020-06-24T10:19:48.005Z", + "@metadata": { + "beat": "metricbeat", + "type": "_doc", + "version": "8.0.0", + "raw_index": "metrics-apache.status-default" + }, + "metricset": { + "name": "status", + "period": 10000 + }, + "apache": { + "status": { + "connections": { + "total": 0, + "async": { + "writing": 0, + "keep_alive": 0, + "closing": 0 + } + }, + "total_kbytes": 128, + "cpu": { + "children_user": 0, + "children_system": 0, + "load": 0.185185, + "user": 1.11, + "system": 1.79 + }, + "scoreboard": { + "logging": 0, + "idle_cleanup": 0, + "starting_up": 0, + "reading_request": 0, + "dns_lookup": 0, + "closing_connection": 0, + "gracefully_finishing": 0, + "sending_reply": 1, + "keepalive": 0, + "total": 400, + "open_slot": 325, + "waiting_for_connection": 74 + }, + "workers": { + "busy": 1, + "idle": 74 + }, + "bytes_per_sec": 83.6986, + "hostname": "127.0.0.1:8088", + "uptime": { + "server_uptime": 1566, + "uptime": 1566 + }, + "total_accesses": 1393, + "bytes_per_request": 94.0933, + "requests_per_sec": 0.889527, + "load": { + "1": 3.58, + "5": 3.54, + "15": 2.79 + } + } + }, + "service": { + "address": "127.0.0.1:8088", + "type": "apache" + }, + "event": { + "duration": 2381832, + "dataset": "apache.status", + "module": "apache" + }, + "dataset": { + "type": "metrics", + "name": "apache.status", + "namespace": "default" + }, + "stream": { + "dataset": "apache.status", + "namespace": "default", + "type": "metrics" + }, + "ecs": { + "version": "1.5.0" + }, + "agent": { + "type": "metricbeat", + "version": "8.0.0", + "ephemeral_id": "685f03e4-76e7-4d05-b398-8454b8964681", + "id": "a74466da-3ea4-44f9-aea0-11c5e4b920be", + "name": "MacBook-Elastic.local" + } +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/docs/README.md new file mode 100644 index 0000000000000..0de86eb122b8b --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/docs/README.md @@ -0,0 +1,238 @@ +# Apache Integration + +This integration periodically fetches metrics from [Apache](https://httpd.apache.org/) servers. It can parse access and error +logs created by the Apache server. + +## Compatibility + +The Apache datasets were tested with Apache 2.4.12 and 2.4.20 and are expected to work with +all versions >= 2.2.31 and >= 2.4.16. + +## Logs + +### Access Logs + +Access logs collects the Apache access logs. + +**Exported fields** + +| Field | Description | Type | +|---|---|---| +| @timestamp | Event timestamp. | date | +| apache.access.ssl.cipher | SSL cipher name. | keyword | +| apache.access.ssl.protocol | SSL protocol version. | keyword | +| data_stream.dataset | Data stream dataset. | constant_keyword | +| data_stream.namespace | Data stream namespace. | constant_keyword | +| data_stream.type | Data stream type. | constant_keyword | +| http.request.method | HTTP request method. Prior to ECS 1.6.0 the following guidance was provided: "The field value must be normalized to lowercase for querying." As of ECS 1.6.0, the guidance is deprecated because the original case of the method may be useful in anomaly detection. Original case will be mandated in ECS 2.0.0 | keyword | +| http.request.referrer | Referrer for this HTTP request. | keyword | +| http.response.body.bytes | Size in bytes of the response body. | long | +| http.response.status_code | HTTP response status code. | long | +| http.version | HTTP version. | keyword | +| log.level | Original log level of the log event. If the source of the event provides a log level or textual severity, this is the one that goes in `log.level`. If your source doesn't specify one, you may put your event transport's severity here (e.g. Syslog severity). Some examples are `warn`, `err`, `i`, `informational`. | keyword | +| message | For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message. | text | +| process.pid | Process id. | long | +| process.thread.id | Thread ID. | long | +| source.address | Some event source addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is. | keyword | +| source.geo.city_name | City name. | keyword | +| source.geo.continent_name | Name of the continent. | keyword | +| source.geo.country_iso_code | Country ISO code. | keyword | +| source.geo.location | Longitude and latitude. | geo_point | +| source.geo.region_iso_code | Region ISO code. | keyword | +| source.geo.region_name | Region name. | keyword | +| url.original | Unmodified original url as seen in the event source. Note that in network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. This field is meant to represent the URL as it was observed, complete or not. | keyword | +| user.name | Short name or login of the user. | keyword | +| user_agent.device.name | Name of the device. | keyword | +| user_agent.name | Name of the user agent. | keyword | +| user_agent.original | Unparsed user_agent string. | keyword | +| user_agent.os.name | Operating system name, without the version. | keyword | +| user_agent.os.version | Operating system version as a raw string. | keyword | +| user_agent.version | Version of the user agent. | keyword | + + +### Error Logs + +Error logs collects the Apache error logs. + +**Exported fields** + +| Field | Description | Type | +|---|---|---| +| @timestamp | Event timestamp. | date | +| apache.error.module | The module producing the logged message. | keyword | +| data_stream.dataset | Data stream dataset. | constant_keyword | +| data_stream.namespace | Data stream namespace. | constant_keyword | +| data_stream.type | Data stream type. | constant_keyword | +| http.request.method | HTTP request method. Prior to ECS 1.6.0 the following guidance was provided: "The field value must be normalized to lowercase for querying." As of ECS 1.6.0, the guidance is deprecated because the original case of the method may be useful in anomaly detection. Original case will be mandated in ECS 2.0.0 | keyword | +| http.request.referrer | Referrer for this HTTP request. | keyword | +| http.response.body.bytes | Size in bytes of the response body. | long | +| http.response.status_code | HTTP response status code. | long | +| http.version | HTTP version. | keyword | +| log.level | Original log level of the log event. If the source of the event provides a log level or textual severity, this is the one that goes in `log.level`. If your source doesn't specify one, you may put your event transport's severity here (e.g. Syslog severity). Some examples are `warn`, `err`, `i`, `informational`. | keyword | +| message | For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message. | text | +| process.pid | Process id. | long | +| process.thread.id | Thread ID. | long | +| source.address | Some event source addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is. | keyword | +| source.geo.city_name | City name. | keyword | +| source.geo.continent_name | Name of the continent. | keyword | +| source.geo.country_iso_code | Country ISO code. | keyword | +| source.geo.location | Longitude and latitude. | geo_point | +| source.geo.region_iso_code | Region ISO code. | keyword | +| source.geo.region_name | Region name. | keyword | +| url.original | Unmodified original url as seen in the event source. Note that in network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. This field is meant to represent the URL as it was observed, complete or not. | keyword | +| user.name | Short name or login of the user. | keyword | +| user_agent.device.name | Name of the device. | keyword | +| user_agent.name | Name of the user agent. | keyword | +| user_agent.original | Unparsed user_agent string. | keyword | +| user_agent.os.name | Operating system name, without the version. | keyword | + + +## Metrics + +### Status Metrics + +The server status stream collects data from the Apache Status module. It scrapes the status data from the web page +generated by the `mod_status` module. + +An example event for `status` looks as following: + +```$json +{ + "@metadata": { + "beat": "metricbeat", + "raw_index": "metrics-apache.status-default", + "type": "_doc", + "version": "8.0.0" + }, + "@timestamp": "2020-06-24T10:19:48.005Z", + "agent": { + "ephemeral_id": "685f03e4-76e7-4d05-b398-8454b8964681", + "id": "a74466da-3ea4-44f9-aea0-11c5e4b920be", + "name": "MacBook-Elastic.local", + "type": "metricbeat", + "version": "8.0.0" + }, + "apache": { + "status": { + "bytes_per_request": 94.0933, + "bytes_per_sec": 83.6986, + "connections": { + "async": { + "closing": 0, + "keep_alive": 0, + "writing": 0 + }, + "total": 0 + }, + "cpu": { + "children_system": 0, + "children_user": 0, + "load": 0.185185, + "system": 1.79, + "user": 1.11 + }, + "hostname": "127.0.0.1:8088", + "load": { + "1": 3.58, + "15": 2.79, + "5": 3.54 + }, + "requests_per_sec": 0.889527, + "scoreboard": { + "closing_connection": 0, + "dns_lookup": 0, + "gracefully_finishing": 0, + "idle_cleanup": 0, + "keepalive": 0, + "logging": 0, + "open_slot": 325, + "reading_request": 0, + "sending_reply": 1, + "starting_up": 0, + "total": 400, + "waiting_for_connection": 74 + }, + "total_accesses": 1393, + "total_kbytes": 128, + "uptime": { + "server_uptime": 1566, + "uptime": 1566 + }, + "workers": { + "busy": 1, + "idle": 74 + } + } + }, + "dataset": { + "name": "apache.status", + "namespace": "default", + "type": "metrics" + }, + "ecs": { + "version": "1.5.0" + }, + "event": { + "dataset": "apache.status", + "duration": 2381832, + "module": "apache" + }, + "metricset": { + "name": "status", + "period": 10000 + }, + "service": { + "address": "127.0.0.1:8088", + "type": "apache" + }, + "stream": { + "dataset": "apache.status", + "namespace": "default", + "type": "metrics" + } +} +``` + +**Exported fields** + +| Field | Description | Type | +|---|---|---| +| @timestamp | Event timestamp. | date | +| apache.status.bytes_per_request | Bytes per request. | scaled_float | +| apache.status.bytes_per_sec | Bytes per second. | scaled_float | +| apache.status.connections.async.closing | Async closed connections. | long | +| apache.status.connections.async.keep_alive | Async keeped alive connections. | long | +| apache.status.connections.async.writing | Async connection writing. | long | +| apache.status.connections.total | Total connections. | long | +| apache.status.cpu.children_system | CPU of children system. | scaled_float | +| apache.status.cpu.children_user | CPU of children user. | scaled_float | +| apache.status.cpu.load | CPU Load. | scaled_float | +| apache.status.cpu.system | System cpu. | scaled_float | +| apache.status.cpu.user | CPU user load. | scaled_float | +| apache.status.hostname | Apache hostname. | keyword | +| apache.status.load.1 | Load average for the last minute. | scaled_float | +| apache.status.load.15 | Load average for the last 15 minutes. | scaled_float | +| apache.status.load.5 | Load average for the last 5 minutes. | scaled_float | +| apache.status.requests_per_sec | Requests per second. | scaled_float | +| apache.status.scoreboard.closing_connection | Closing connections. | long | +| apache.status.scoreboard.dns_lookup | Dns Lookups. | long | +| apache.status.scoreboard.gracefully_finishing | Gracefully finishing. | long | +| apache.status.scoreboard.idle_cleanup | Idle cleanups. | long | +| apache.status.scoreboard.keepalive | Keep alive. | long | +| apache.status.scoreboard.logging | Logging | long | +| apache.status.scoreboard.open_slot | Open slots. | long | +| apache.status.scoreboard.reading_request | Reading requests. | long | +| apache.status.scoreboard.sending_reply | Sending Reply. | long | +| apache.status.scoreboard.starting_up | Starting up. | long | +| apache.status.scoreboard.total | Total. | long | +| apache.status.scoreboard.waiting_for_connection | Waiting for connections. | long | +| apache.status.total_accesses | Total number of access requests. | long | +| apache.status.total_kbytes | Total number of kilobytes served. | long | +| apache.status.uptime.server_uptime | Server uptime in seconds. | long | +| apache.status.uptime.uptime | Server uptime. | long | +| apache.status.workers.busy | Number of busy workers. | long | +| apache.status.workers.idle | Number of idle workers. | long | +| data_stream.dataset | Data stream dataset. | constant_keyword | +| data_stream.namespace | Data stream namespace. | constant_keyword | +| data_stream.type | Data stream type. | constant_keyword | + diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/apache_httpd_server_status.png b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/apache_httpd_server_status.png new file mode 100644 index 0000000000000..b28bbecb34c46 Binary files /dev/null and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/apache_httpd_server_status.png differ diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/kibana-apache.png b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/kibana-apache.png new file mode 100644 index 0000000000000..badfee933a5ea Binary files /dev/null and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/kibana-apache.png differ diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/logo_apache.svg b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/logo_apache.svg new file mode 100644 index 0000000000000..384761f64108a --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/img/logo_apache.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/dashboard/apache-Logs-Apache-Dashboard-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/dashboard/apache-Logs-Apache-Dashboard-ecs.json new file mode 100644 index 0000000000000..61339d46447e3 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/dashboard/apache-Logs-Apache-Dashboard-ecs.json @@ -0,0 +1,56 @@ +{ + "attributes": { + "description": "Logs Apache integration dashboard", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"highlightAll\":true,\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false}", + "panelsJSON": "[{\"embeddableConfig\":{\"mapBounds\":{\"bottom_right\":{\"lat\":-3.864254615721396,\"lon\":205.3125},\"top_left\":{\"lat\":67.7427590666639,\"lon\":-205.6640625}},\"mapCenter\":[40.713955826286046,-0.17578125],\"mapCollar\":{\"bottom_right\":{\"lat\":-39.667755,\"lon\":180},\"top_left\":{\"lat\":90,\"lon\":-180},\"zoom\":2},\"mapZoom\":2},\"gridData\":{\"h\":12,\"i\":\"1\",\"w\":48,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"panelRefName\":\"panel_0\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"2\",\"w\":32,\"x\":0,\"y\":20},\"panelIndex\":\"2\",\"panelRefName\":\"panel_1\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"3\",\"w\":16,\"x\":32,\"y\":20},\"panelIndex\":\"3\",\"panelRefName\":\"panel_2\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":8,\"i\":\"4\",\"w\":8,\"x\":40,\"y\":12},\"panelIndex\":\"4\",\"panelRefName\":\"panel_3\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":8,\"i\":\"5\",\"w\":48,\"x\":0,\"y\":32},\"panelIndex\":\"5\",\"panelRefName\":\"panel_4\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":8,\"i\":\"6\",\"w\":40,\"x\":0,\"y\":12},\"panelIndex\":\"6\",\"panelRefName\":\"panel_5\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"columns\":[\"source.address\",\"log.level\",\"apache2.error.integration\",\"message\"],\"sort\":[\"@timestamp\",\"desc\"]},\"gridData\":{\"h\":12,\"i\":\"7\",\"w\":48,\"x\":0,\"y\":40},\"panelIndex\":\"7\",\"panelRefName\":\"panel_6\",\"version\":\"7.3.0\"}]", + "timeRestore": false, + "title": "[Logs Apache] Access and error logs ECS", + "version": 1 + }, + "id": "apache-Logs-Apache-Dashboard-ecs", + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "Apache-access-unique-IPs-map-ecs", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "Apache-response-codes-of-top-URLs-ecs", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "Apache-browsers-ecs", + "name": "panel_2", + "type": "visualization" + }, + { + "id": "Apache-operating-systems-ecs", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "Apache-error-logs-over-time-ecs", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "Apache-response-codes-over-time-ecs", + "name": "panel_5", + "type": "visualization" + }, + { + "id": "Apache-errors-log-ecs", + "name": "panel_6", + "type": "search" + } + ], + "type": "dashboard" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/dashboard/apache-Metrics-Apache-HTTPD-server-status-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/dashboard/apache-Metrics-Apache-HTTPD-server-status-ecs.json new file mode 100644 index 0000000000000..92c4a5b3ee391 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/dashboard/apache-Metrics-Apache-HTTPD-server-status-ecs.json @@ -0,0 +1,56 @@ +{ + "attributes": { + "description": "Overview of Apache server status", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"highlightAll\":true,\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false}", + "panelsJSON": "[{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"1\",\"w\":24,\"x\":24,\"y\":36},\"panelIndex\":\"1\",\"panelRefName\":\"panel_0\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}},\"gridData\":{\"h\":12,\"i\":\"2\",\"w\":12,\"x\":0,\"y\":0},\"panelIndex\":\"2\",\"panelRefName\":\"panel_1\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":36},\"panelIndex\":\"3\",\"panelRefName\":\"panel_2\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"4\",\"w\":48,\"x\":0,\"y\":24},\"panelIndex\":\"4\",\"panelRefName\":\"panel_3\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"vis\":{\"defaultColors\":{\"0 - 100\":\"rgb(0,104,55)\"}}},\"gridData\":{\"h\":12,\"i\":\"5\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"5\",\"panelRefName\":\"panel_4\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{\"vis\":{\"defaultColors\":{\"0 - 100\":\"rgb(0,104,55)\"}}},\"gridData\":{\"h\":12,\"i\":\"6\",\"w\":12,\"x\":12,\"y\":0},\"panelIndex\":\"6\",\"panelRefName\":\"panel_5\",\"version\":\"7.3.0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":12,\"i\":\"7\",\"w\":48,\"x\":0,\"y\":12},\"panelIndex\":\"7\",\"panelRefName\":\"panel_6\",\"version\":\"7.3.0\"}]", + "timeRestore": false, + "title": "[Metrics Apache] Overview ECS", + "version": 1 + }, + "id": "apache-Metrics-Apache-HTTPD-server-status-ecs", + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "Apache-HTTPD-CPU-ecs", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "Apache-HTTPD-Hostname-list-ecs", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "Apache-HTTPD-Load1-slash-5-slash-15-ecs", + "name": "panel_2", + "type": "visualization" + }, + { + "id": "Apache-HTTPD-Scoreboard-ecs", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "Apache-HTTPD-Total-accesses-and-kbytes-ecs", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "Apache-HTTPD-Uptime-ecs", + "name": "panel_5", + "type": "visualization" + }, + { + "id": "Apache-HTTPD-Workers-ecs", + "name": "panel_6", + "type": "visualization" + } + ], + "type": "dashboard" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-HTTPD-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-HTTPD-ecs.json new file mode 100644 index 0000000000000..649a2669e6bd6 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-HTTPD-ecs.json @@ -0,0 +1,32 @@ +{ + "attributes": { + "columns": [ + "_source" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"highlight\":{\"fields\":{\"*\":{}},\"fragment_size\":2147483647,\"post_tags\":[\"@/kibana-highlighted-field@\"],\"pre_tags\":[\"@kibana-highlighted-field@\"],\"require_field_match\":false},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"query\":{\"language\":\"kuery\",\"query\":\"(data_stream.dataset:apache.status)\"}}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Apache HTTPD ECS", + "version": 1 + }, + "id": "Apache-HTTPD-ecs", + "migrationVersion": { + "search": "7.4.0" + }, + "references": [ + { + "id": "metrics-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-access-logs-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-access-logs-ecs.json new file mode 100644 index 0000000000000..d91a350de171e --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-access-logs-ecs.json @@ -0,0 +1,35 @@ +{ + "attributes": { + "columns": [ + "source.address", + "http.request.method", + "url.original", + "http.response.status_code" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"highlight\":{\"fields\":{\"*\":{}},\"fragment_size\":2147483647,\"post_tags\":[\"@/kibana-highlighted-field@\"],\"pre_tags\":[\"@kibana-highlighted-field@\"],\"require_field_match\":false},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"query\":{\"language\":\"kuery\",\"query\":\"data_stream.dataset:apache.access\"}}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Apache access logs [Logs Apache] ECS", + "version": 1 + }, + "id": "Apache-access-logs-ecs", + "migrationVersion": { + "search": "7.4.0" + }, + "references": [ + { + "id": "logs-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-errors-log-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-errors-log-ecs.json new file mode 100644 index 0000000000000..a7244db0b8e93 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/search/Apache-errors-log-ecs.json @@ -0,0 +1,35 @@ +{ + "attributes": { + "columns": [ + "source.address", + "log.level", + "apache2.error.integration", + "message" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[],\"highlight\":{\"fields\":{\"*\":{}},\"fragment_size\":2147483647,\"post_tags\":[\"@/kibana-highlighted-field@\"],\"pre_tags\":[\"@kibana-highlighted-field@\"],\"require_field_match\":false},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"query\":{\"language\":\"kuery\",\"query\":\"data_stream.dataset:apache.error\"}}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "Apache errors log [Logs Apache] ECS", + "version": 1 + }, + "id": "Apache-errors-log-ecs", + "migrationVersion": { + "search": "7.4.0" + }, + "references": [ + { + "id": "logs-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-CPU-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-CPU-ecs.json new file mode 100644 index 0000000000000..7200bffec0d0d --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-CPU-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "CPU usage [Metrics Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"id\":\"1\",\"params\":{\"customLabel\":\"CPU load\",\"field\":\"apache.status.cpu.load\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"id\":\"3\",\"params\":{\"field\":\"apache.status.hostname\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"split\",\"type\":\"terms\"},{\"id\":\"4\",\"params\":{\"customLabel\":\"CPU user\",\"field\":\"apache.status.cpu.user\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"5\",\"params\":{\"customLabel\":\"CPU system\",\"field\":\"apache.status.cpu.system\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"6\",\"params\":{\"customLabel\":\"CPU children user\",\"field\":\"apache.status.cpu.children_user\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"7\",\"params\":{\"customLabel\":\"CPU children system\",\"field\":\"apache.status.cpu.children_system\"},\"schema\":\"metric\",\"type\":\"avg\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"defaultYExtents\":false,\"drawLinesBetweenPoints\":true,\"interpolate\":\"linear\",\"radiusRatio\":9,\"row\":true,\"scale\":\"linear\",\"setYExtents\":false,\"shareYAxis\":true,\"showCircles\":true,\"smoothLines\":false,\"times\":[],\"yAxis\":{}},\"title\":\"Apache HTTPD - CPU ECS\",\"type\":\"line\"}" + }, + "id": "Apache-HTTPD-CPU-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-HTTPD-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Hostname-list-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Hostname-list-ecs.json new file mode 100644 index 0000000000000..6cba780a2a121 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Hostname-list-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Hostname list [Metrics Apache] ECS", + "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}", + "version": 1, + "visState": "{\"aggs\":[{\"id\":\"1\",\"params\":{\"customLabel\":\"Events count\"},\"schema\":\"metric\",\"type\":\"count\"},{\"id\":\"2\",\"params\":{\"customLabel\":\"Apache HTTD Hostname\",\"field\":\"apache.status.hostname\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"bucket\",\"type\":\"terms\"}],\"listeners\":{},\"params\":{\"perPage\":10,\"showMeticsAtAllLevels\":false,\"showPartialRows\":false,\"sort\":{\"columnIndex\":null,\"direction\":null}},\"title\":\"Apache HTTPD - Hostname list ECS\",\"type\":\"table\"}" + }, + "id": "Apache-HTTPD-Hostname-list-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-HTTPD-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Load1-slash-5-slash-15-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Load1-slash-5-slash-15-ecs.json new file mode 100644 index 0000000000000..1e6878e6c5cb1 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Load1-slash-5-slash-15-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Load1/5/15 [Metrics Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"id\":\"1\",\"params\":{\"customLabel\":\"Load 5\",\"field\":\"apache.status.load.5\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"id\":\"3\",\"params\":{\"customLabel\":\"Load 1\",\"field\":\"apache.status.load.1\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"4\",\"params\":{\"customLabel\":\"Load 15\",\"field\":\"apache.status.load.15\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"5\",\"params\":{\"customLabel\":\"Hostname\",\"field\":\"apache.status.hostname\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"split\",\"type\":\"terms\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"defaultYExtents\":false,\"drawLinesBetweenPoints\":true,\"interpolate\":\"linear\",\"radiusRatio\":9,\"row\":true,\"scale\":\"linear\",\"setYExtents\":false,\"shareYAxis\":true,\"showCircles\":true,\"smoothLines\":false,\"times\":[],\"yAxis\":{}},\"title\":\"Apache HTTPD - Load1/5/15 ECS\",\"type\":\"line\"}" + }, + "id": "Apache-HTTPD-Load1-slash-5-slash-15-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-HTTPD-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Scoreboard-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Scoreboard-ecs.json new file mode 100644 index 0000000000000..990e2610a6755 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Scoreboard-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Scoreboard [Metrics Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"id\":\"1\",\"params\":{\"customLabel\":\"Closing connection\",\"field\":\"apache.status.scoreboard.closing_connection\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"id\":\"3\",\"params\":{\"customLabel\":\"Hostname\",\"field\":\"apache.status.hostname\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"split\",\"type\":\"terms\"},{\"id\":\"4\",\"params\":{\"customLabel\":\"DNS lookup\",\"field\":\"apache.status.scoreboard.dns_lookup\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"5\",\"params\":{\"customLabel\":\"Gracefully finishing\",\"field\":\"apache.status.scoreboard.gracefully_finishing\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"6\",\"params\":{\"customLabel\":\"Idle cleanup\",\"field\":\"apache.status.scoreboard.idle_cleanup\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"7\",\"params\":{\"customLabel\":\"Keepalive\",\"field\":\"apache.status.scoreboard.keepalive\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"8\",\"params\":{\"customLabel\":\"Logging\",\"field\":\"apache.status.scoreboard.logging\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"9\",\"params\":{\"customLabel\":\"Open slot\",\"field\":\"apache.status.scoreboard.open_slot\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"10\",\"params\":{\"customLabel\":\"Reading request\",\"field\":\"apache.status.scoreboard.reading_request\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"11\",\"params\":{\"customLabel\":\"Sending reply\",\"field\":\"apache.status.scoreboard.sending_reply\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"12\",\"params\":{\"customLabel\":\"Starting up\",\"field\":\"apache.status.scoreboard.starting_up\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"13\",\"params\":{\"customLabel\":\"Total\",\"field\":\"apache.status.scoreboard.total\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"14\",\"params\":{\"customLabel\":\"Waiting for connection\",\"field\":\"apache.status.scoreboard.waiting_for_connection\"},\"schema\":\"metric\",\"type\":\"avg\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"defaultYExtents\":false,\"drawLinesBetweenPoints\":true,\"interpolate\":\"linear\",\"radiusRatio\":9,\"row\":true,\"scale\":\"linear\",\"setYExtents\":false,\"shareYAxis\":true,\"showCircles\":true,\"smoothLines\":false,\"times\":[],\"yAxis\":{}},\"title\":\"Apache HTTPD - Scoreboard ECS\",\"type\":\"line\"}" + }, + "id": "Apache-HTTPD-Scoreboard-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-HTTPD-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Total-accesses-and-kbytes-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Total-accesses-and-kbytes-ecs.json new file mode 100644 index 0000000000000..ad305d971b575 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Total-accesses-and-kbytes-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Total accesses and kbytes [Metrics Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"id\":\"1\",\"params\":{\"customLabel\":\"Total kbytes\",\"field\":\"apache.status.total_kbytes\"},\"schema\":\"metric\",\"type\":\"max\"},{\"id\":\"2\",\"params\":{\"customLabel\":\"Total accesses\",\"field\":\"apache.status.total_accesses\"},\"schema\":\"metric\",\"type\":\"max\"}],\"listeners\":{},\"params\":{\"fontSize\":60,\"handleNoResults\":true},\"title\":\"Apache HTTPD - Total accesses and kbytes ECS\",\"type\":\"metric\"}" + }, + "id": "Apache-HTTPD-Total-accesses-and-kbytes-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-HTTPD-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Uptime-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Uptime-ecs.json new file mode 100644 index 0000000000000..734fdef3c040a --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Uptime-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Uptime [Metrics Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"id\":\"1\",\"params\":{\"customLabel\":\"Uptime\",\"field\":\"apache.status.uptime.uptime\"},\"schema\":\"metric\",\"type\":\"max\"},{\"id\":\"2\",\"params\":{\"customLabel\":\"Server uptime\",\"field\":\"apache.status.uptime.server_uptime\"},\"schema\":\"metric\",\"type\":\"max\"}],\"listeners\":{},\"params\":{\"fontSize\":60,\"handleNoResults\":true},\"title\":\"Apache HTTPD - Uptime ECS\",\"type\":\"metric\"}" + }, + "id": "Apache-HTTPD-Uptime-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-HTTPD-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Workers-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Workers-ecs.json new file mode 100644 index 0000000000000..15108d3dd95ce --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-HTTPD-Workers-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Workers [Metrics Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"id\":\"1\",\"params\":{\"customLabel\":\"Busy workers\",\"field\":\"apache.status.workers.busy\"},\"schema\":\"metric\",\"type\":\"avg\"},{\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"id\":\"3\",\"params\":{\"customLabel\":\"Hostname\",\"field\":\"apache.status.hostname\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"split\",\"type\":\"terms\"},{\"id\":\"4\",\"params\":{\"customLabel\":\"Idle workers\",\"field\":\"apache.status.workers.idle\"},\"schema\":\"metric\",\"type\":\"avg\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"defaultYExtents\":false,\"drawLinesBetweenPoints\":true,\"interpolate\":\"linear\",\"radiusRatio\":9,\"row\":true,\"scale\":\"linear\",\"setYExtents\":false,\"shareYAxis\":true,\"showCircles\":true,\"smoothLines\":false,\"times\":[],\"yAxis\":{}},\"title\":\"Apache HTTPD - Workers ECS\",\"type\":\"line\"}" + }, + "id": "Apache-HTTPD-Workers-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-HTTPD-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-access-unique-IPs-map-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-access-unique-IPs-map-ecs.json new file mode 100644 index 0000000000000..c14348f6fedce --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-access-unique-IPs-map-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Unique IPs map [Logs Apache] ECS", + "uiStateJSON": "{\"mapCenter\":[14.944784875088372,5.09765625]}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{\"field\":\"source.address\"},\"schema\":\"metric\",\"type\":\"cardinality\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"autoPrecision\":true,\"field\":\"source.geo.location\"},\"schema\":\"segment\",\"type\":\"geohash_grid\"}],\"listeners\":{},\"params\":{\"addTooltip\":true,\"heatBlur\":15,\"heatMaxZoom\":16,\"heatMinOpacity\":0.1,\"heatNormalizeData\":true,\"heatRadius\":25,\"isDesaturated\":true,\"legendPosition\":\"bottomright\",\"mapCenter\":[15,5],\"mapType\":\"Scaled Circle Markers\",\"mapZoom\":2,\"wms\":{\"enabled\":false,\"options\":{\"attribution\":\"Maps provided by USGS\",\"format\":\"image/png\",\"layers\":\"0\",\"styles\":\"\",\"transparent\":true,\"version\":\"1.3.0\"},\"url\":\"https://basemap.nationalmap.gov/arcgis/services/USGSTopo/MapServer/WMSServer\"}},\"title\":\"Apache access unique IPs map ECS\",\"type\":\"tile_map\"}" + }, + "id": "Apache-access-unique-IPs-map-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-access-logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-browsers-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-browsers-ecs.json new file mode 100644 index 0000000000000..75a9e7629a3c0 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-browsers-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Browsers breakdown [Logs Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{\"field\":\"source.address\"},\"schema\":\"metric\",\"type\":\"cardinality\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"field\":\"user_agent.name\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"field\":\"user_agent.version\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTooltip\":true,\"isDonut\":true,\"legendPosition\":\"bottom\",\"shareYAxis\":true},\"title\":\"Apache browsers ECS\",\"type\":\"pie\"}" + }, + "id": "Apache-browsers-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-access-logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-error-logs-over-time-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-error-logs-over-time-ecs.json new file mode 100644 index 0000000000000..72a30c25079b5 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-error-logs-over-time-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Error logs over time [Logs Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"defaultYExtents\":false,\"legendPosition\":\"right\",\"mode\":\"stacked\",\"scale\":\"linear\",\"setYExtents\":false,\"shareYAxis\":true,\"times\":[],\"yAxis\":{}},\"title\":\"Apache error logs over time ECS\",\"type\":\"histogram\"}" + }, + "id": "Apache-error-logs-over-time-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-errors-log-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-operating-systems-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-operating-systems-ecs.json new file mode 100644 index 0000000000000..4c2e8ff803894 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-operating-systems-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Operating systems breakdown [Logs Apache] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{\"field\":\"source.address\"},\"schema\":\"metric\",\"type\":\"cardinality\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"field\":\"user_agent.os.name\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"field\":\"user_agent.os.version\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTooltip\":true,\"isDonut\":true,\"legendPosition\":\"bottom\",\"shareYAxis\":true},\"title\":\"Apache operating systems ECS\",\"type\":\"pie\"}" + }, + "id": "Apache-operating-systems-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-access-logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-response-codes-of-top-URLs-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-response-codes-of-top-URLs-ecs.json new file mode 100644 index 0000000000000..cc937358b547c --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-response-codes-of-top-URLs-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Top URLs by response code [Logs Apache] ECS", + "uiStateJSON": "{\"vis\":{\"colors\":{\"200\":\"#7EB26D\",\"404\":\"#EF843C\"}}}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"URL\",\"field\":\"url.original\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"split\",\"type\":\"terms\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"field\":\"http.response.status_code\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTooltip\":true,\"isDonut\":false,\"legendPosition\":\"right\",\"row\":false,\"shareYAxis\":true},\"title\":\"Apache response codes of top URLs ECS\",\"type\":\"pie\"}" + }, + "id": "Apache-response-codes-of-top-URLs-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-access-logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-response-codes-over-time-ecs.json b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-response-codes-over-time-ecs.json new file mode 100644 index 0000000000000..65e16c8633f6a --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/kibana/visualization/Apache-response-codes-over-time-ecs.json @@ -0,0 +1,25 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Response codes over time [Logs Apache] ECS", + "uiStateJSON": "{\"vis\":{\"colors\":{\"200\":\"#629E51\",\"404\":\"#EF843C\"}}}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"field\":\"http.response.status_code\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"listeners\":{},\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"defaultYExtents\":false,\"legendPosition\":\"right\",\"mode\":\"stacked\",\"scale\":\"linear\",\"setYExtents\":false,\"shareYAxis\":true,\"times\":[],\"yAxis\":{}},\"title\":\"Apache response codes over time ECS\",\"type\":\"histogram\"}" + }, + "id": "Apache-response-codes-over-time-ecs", + "migrationVersion": { + "visualization": "7.8.0" + }, + "references": [ + { + "id": "Apache-access-logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/manifest.yml new file mode 100644 index 0000000000000..9a98a9a907d20 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache-0.1.4/manifest.yml @@ -0,0 +1,48 @@ +format_version: 1.0.0 +name: apache +version: 0.1.4 +license: basic +description: Apache Integration +type: integration +categories: + - web +release: experimental +removable: true +conditions: + kibana.version: '^7.9.0' +screenshots: + - src: /img/kibana-apache.png + title: Apache Integration + size: 1215x1199 + type: image/png + - src: /img/apache_httpd_server_status.png + title: Apache HTTPD Server Status + size: 1919x1079 + type: image/png +icons: + - src: /img/logo_apache.svg + title: Apache Logo + size: 32x32 + type: image/svg+xml +config_templates: + - name: apache + title: Apache logs and metrics + description: Collect logs and metrics from Apache instances + inputs: + - type: logfile + title: Collect logs from Apache instances + description: Collecting Apache access and error logs + - type: apache/metrics + title: Collect metrics from Apache instances + description: Collecting Apache status metrics + vars: + - name: hosts + type: text + title: Hosts + multi: true + required: true + show_user: true + default: + - http://127.0.0.1 +owner: + github: elastic/integrations-services diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip index fa329e57ec44f..2796c0094ac1e 100644 Binary files a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip differ diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index d5284c23f7fb7..d73904d792955 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 f829002448f33..5e44cae752905 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 0000000000000..d927f93adeffd --- /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 0000000000000..98ccd85b7c15d --- /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/apps/maps/group1/sample_data.js b/x-pack/test/functional/apps/maps/group1/sample_data.js index 62df1d3859a45..a8ebc8ec6ba85 100644 --- a/x-pack/test/functional/apps/maps/group1/sample_data.js +++ b/x-pack/test/functional/apps/maps/group1/sample_data.js @@ -29,61 +29,16 @@ export default function ({ getPageObjects, getService, updateBaselines }) { await PageObjects.home.addSampleDataSet('ecommerce'); await PageObjects.home.addSampleDataSet('flights'); await PageObjects.home.addSampleDataSet('logs'); + + // Sample data is shifted to be relative to current time + // This means that a static timerange will return different documents + // Setting the time range to a window larger than the sample data set + // ensures all documents are coverered by time query so the ES results will always be the same const SAMPLE_DATA_RANGE = `[ { - "from": "now-30d", - "to": "now+40d", + "from": "now-180d", + "to": "now+180d", "display": "sample data range" - }, - { - "from": "now/d", - "to": "now/d", - "display": "Today" - }, - { - "from": "now/w", - "to": "now/w", - "display": "This week" - }, - { - "from": "now-15m", - "to": "now", - "display": "Last 15 minutes" - }, - { - "from": "now-30m", - "to": "now", - "display": "Last 30 minutes" - }, - { - "from": "now-1h", - "to": "now", - "display": "Last 1 hour" - }, - { - "from": "now-24h", - "to": "now", - "display": "Last 24 hours" - }, - { - "from": "now-7d", - "to": "now", - "display": "Last 7 days" - }, - { - "from": "now-30d", - "to": "now", - "display": "Last 30 days" - }, - { - "from": "now-90d", - "to": "now", - "display": "Last 90 days" - }, - { - "from": "now-1y", - "to": "now", - "display": "Last 1 year" } ]`; diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index 6e3e98171b109..3f143c0163fb3 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -62,6 +62,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); const elasticChart = getService('elasticChart'); + const browser = getService('browser'); describe('anomaly explorer', function () { this.tags(['ml']); @@ -213,6 +214,19 @@ export default function ({ getService }: FtrProviderContext) { 'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t' ); + await ml.testExecution.logTestStep('restores app state from the URL state'); + await browser.refresh(); + await elasticChart.setNewChartUiDebugFlag(true); + await ml.swimLane.waitForSwimLanesToLoad(); + await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { + x: [1454846400000, 1454860800000], + y: ['Overall'], + }); + await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', ['EGF', 'DAL']); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(5); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2); + await ml.anomaliesTable.assertTableRowsCount(4); + await ml.testExecution.logTestStep('clears the selection'); await ml.anomalyExplorer.clearSwimLaneSelection(); await ml.swimLane.waitForSwimLanesToLoad(); @@ -274,6 +288,22 @@ export default function ({ getService }: FtrProviderContext) { y: ['Overall'], }); + await ml.testExecution.logTestStep('restores app state from the URL state'); + await browser.refresh(); + await elasticChart.setNewChartUiDebugFlag(true); + await ml.swimLane.waitForSwimLanesToLoad(); + await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, { + x: [1454817600000, 1454832000000], + y: ['AAL'], + }); + await ml.anomaliesTable.assertTableRowsCount(1); + await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1); + await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(1); + await ml.swimLane.assertSelection(overallSwimLaneTestSubj, { + x: [1454817600000, 1454832000000], + y: ['Overall'], + }); + await ml.testExecution.logTestStep('clears the selection'); await ml.anomalyExplorer.clearSwimLaneSelection(); await ml.swimLane.waitForSwimLanesToLoad(); diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index fcb7a22e81d4e..b7e58ba13b151 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/es_archives/cases/default/mappings.json b/x-pack/test/functional/es_archives/cases/default/mappings.json index 28d9daff50d94..c25603894011e 100644 --- a/x-pack/test/functional/es_archives/cases/default/mappings.json +++ b/x-pack/test/functional/es_archives/cases/default/mappings.json @@ -298,13 +298,8 @@ } }, "type": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 256, + "type": "keyword" }, "updated_at": { "type": "date" diff --git a/x-pack/test/functional/es_archives/reporting/unmapped_fields/data.json b/x-pack/test/functional/es_archives/reporting/unmapped_fields/data.json new file mode 100644 index 0000000000000..9df8b1c012e86 --- /dev/null +++ b/x-pack/test/functional/es_archives/reporting/unmapped_fields/data.json @@ -0,0 +1,25 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "recipes", + "source": { + "text":"text1", + "unmapped": "unmapped1" + } + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "recipes", + "source": { + "text":"text2", + "nested": { + "unmapped": "unmapped2" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/reporting/unmapped_fields/mappings.json b/x-pack/test/functional/es_archives/reporting/unmapped_fields/mappings.json new file mode 100644 index 0000000000000..f7cf22b6e4198 --- /dev/null +++ b/x-pack/test/functional/es_archives/reporting/unmapped_fields/mappings.json @@ -0,0 +1,14 @@ +{ + "type": "index", + "value": { + "index": "recipes", + "mappings": { + "dynamic": false, + "properties": { + "text": { + "type": "text" + } + } + } + } +} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/reporting/unmapped_fields.json b/x-pack/test/functional/fixtures/kbn_archiver/reporting/unmapped_fields.json new file mode 100644 index 0000000000000..24c9769834250 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/reporting/unmapped_fields.json @@ -0,0 +1,15 @@ +{ + "attributes": { + "fields": "[]", + "title": "recipes" + }, + "coreMigrationVersion": "8.3.0", + "id": "5c620ea0-dc4f-11ec-972a-bf98ce1eebd7", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2022-05-25T17:23:15.216Z", + "version": "WzE4NywxXQ==" +} diff --git a/x-pack/test/functional/screenshots/baseline/ecommerce_map.png b/x-pack/test/functional/screenshots/baseline/ecommerce_map.png index df13f7d563bfe..76761be5a91bc 100644 Binary files a/x-pack/test/functional/screenshots/baseline/ecommerce_map.png and b/x-pack/test/functional/screenshots/baseline/ecommerce_map.png differ diff --git a/x-pack/test/functional/screenshots/baseline/flights_map.png b/x-pack/test/functional/screenshots/baseline/flights_map.png index 320a0940e2461..dd4f9f4809504 100644 Binary files a/x-pack/test/functional/screenshots/baseline/flights_map.png and b/x-pack/test/functional/screenshots/baseline/flights_map.png differ diff --git a/x-pack/test/functional/screenshots/baseline/web_logs_map.png b/x-pack/test/functional/screenshots/baseline/web_logs_map.png index 6cd145da94a67..c2cfaaf847030 100644 Binary files a/x-pack/test/functional/screenshots/baseline/web_logs_map.png and b/x-pack/test/functional/screenshots/baseline/web_logs_map.png differ diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 8b7d15e96cb26..54ce60ddec848 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 cf529226de895..932d1c4723951 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 ec1f2e089e732..bd0d822e6234e 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 5e80a5769b44d..918133ca53dfc 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 d63739da47d5b..b54f36e020183 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 c7514962c84f7..5e70382418f23 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 5afdb0b00c774..8fb90ccc9338c 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 cffbfb6f4227c..0c1c63ea66acb 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 6dabf813f1d56..443e0616cabe2 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 1860197b43e5b..fe9751dc9c738 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 649465f6a0173..4a8c90abb2ce7 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 0000000000000..7bef4578142e4 --- /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/performance/config.playwright.ts b/x-pack/test/performance/journeys/base.config.ts similarity index 81% rename from x-pack/test/performance/config.playwright.ts rename to x-pack/test/performance/journeys/base.config.ts index 44a53d7be80a1..ea91aa2e6b197 100644 --- a/x-pack/test/performance/config.playwright.ts +++ b/x-pack/test/performance/journeys/base.config.ts @@ -7,17 +7,15 @@ import uuid from 'uuid'; import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from './services'; -import { pageObjects } from './page_objects'; +import { services } from '../services'; +import { pageObjects } from '../page_objects'; // These "secret" values are intentionally written in the source. We would make the APM server accept anonymous traffic if we could const APM_SERVER_URL = 'https://kibana-ops-e2e-perf.apm.us-central1.gcp.cloud.es.io:443'; const APM_PUBLIC_TOKEN = 'CTs9y3cvcfq13bQqsB'; export default async function ({ readConfigFile, log }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); - - const testFiles = [require.resolve('./tests/playwright')]; + const functionalConfig = await readConfigFile(require.resolve('../../functional/config.base.js')); const testBuildId = process.env.BUILDKITE_BUILD_ID ?? `local-${uuid()}`; const testJobId = process.env.BUILDKITE_JOB_ID ?? `local-${uuid()}`; @@ -26,7 +24,6 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext log.info(` 👷‍♀️ BUILD ID ${testBuildId}\n 👷 JOB ID ${testJobId}\n 👷‍♂️ EXECUTION ID:${executionId}`); return { - testFiles, services, pageObjects, servicesRequiredForTestAnalysis: ['performance'], @@ -41,7 +38,7 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext ...functionalConfig.get('kbnTestServer'), serverArgs: [...functionalConfig.get('kbnTestServer.serverArgs')], env: { - ELASTIC_APM_ACTIVE: process.env.ELASTIC_APM_ACTIVE, + ELASTIC_APM_ACTIVE: process.env.TEST_PERFORMANCE_PHASE ? 'true' : 'false', ELASTIC_APM_CONTEXT_PROPAGATION_ONLY: 'false', ELASTIC_APM_ENVIRONMENT: process.env.CI ? 'ci' : 'development', ELASTIC_APM_TRANSACTION_SAMPLE_RATE: '1.0', @@ -58,15 +55,10 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext // for a body with larger size, we might need to reconfigure the APM server to increase the limit. // https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html#long-field-max-length ELASTIC_APM_LONG_FIELD_MAX_LENGTH: 300_000, - ELASTIC_APM_GLOBAL_LABELS: Object.entries({ - ftrConfig: `x-pack/test/performance/tests/config.playwright`, - performancePhase: process.env.TEST_PERFORMANCE_PHASE, - journeyName: process.env.JOURNEY_NAME, + ELASTIC_APM_GLOBAL_LABELS: { testJobId, testBuildId, - }) - .filter(([, v]) => !!v) - .reduce((acc, [k, v]) => (acc ? `${acc},${k}=${v}` : `${k}=${v}`), ''), + }, }, // delay shutdown by 15 seconds to ensure that APM can report the data it collects during test execution delayShutdown: 15_000, diff --git a/x-pack/test/performance/journeys/ecommerce_dashboard/config.ts b/x-pack/test/performance/journeys/ecommerce_dashboard/config.ts new file mode 100644 index 0000000000000..76ffe9a5f19ba --- /dev/null +++ b/x-pack/test/performance/journeys/ecommerce_dashboard/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrConfigProviderContext } from '@kbn/test'; +import { serializeApmGlobalLabels } from '../../utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const performanceConfig = await readConfigFile(require.resolve('../base.config')); + + const testFiles = [require.resolve('./ecommerce_dashboard')]; + + const config = { + testFiles, + ...performanceConfig.getAll(), + }; + + const apmGlobalLabels = { + ...performanceConfig.get('kbnTestServer').env.ELASTIC_APM_GLOBAL_LABELS, + ftrConfig: `x-pack/test/performance/tests/journeys/ecommerce_dashboard/config.ts`, + performancePhase: process.env.TEST_PERFORMANCE_PHASE, + journeyName: 'ecommerce_dashboard', + }; + + return { + ...config, + kbnTestServer: { + ...config.kbnTestServer, + env: { + ...config.kbnTestServer.env, + ELASTIC_APM_GLOBAL_LABELS: serializeApmGlobalLabels(apmGlobalLabels), + }, + }, + }; +} diff --git a/x-pack/test/performance/tests/playwright/ecommerce_dashboard.ts b/x-pack/test/performance/journeys/ecommerce_dashboard/ecommerce_dashboard.ts similarity index 96% rename from x-pack/test/performance/tests/playwright/ecommerce_dashboard.ts rename to x-pack/test/performance/journeys/ecommerce_dashboard/ecommerce_dashboard.ts index 143ca97c6d0b0..67dabe30c90ff 100644 --- a/x-pack/test/performance/tests/playwright/ecommerce_dashboard.ts +++ b/x-pack/test/performance/journeys/ecommerce_dashboard/ecommerce_dashboard.ts @@ -8,7 +8,7 @@ import { Page } from 'playwright'; import { FtrProviderContext } from '../../ftr_provider_context'; import { StepCtx } from '../../services/performance'; -export default function ecommerceDashboard({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { describe('ecommerce_dashboard', () => { it('ecommerce_dashboard', async () => { const performance = getService('performance'); diff --git a/x-pack/test/performance/journeys/flight_dashboard/config.ts b/x-pack/test/performance/journeys/flight_dashboard/config.ts new file mode 100644 index 0000000000000..8640dcd7c1650 --- /dev/null +++ b/x-pack/test/performance/journeys/flight_dashboard/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrConfigProviderContext } from '@kbn/test'; +import { serializeApmGlobalLabels } from '../../utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const performanceConfig = await readConfigFile(require.resolve('../base.config')); + + const testFiles = [require.resolve('./flight_dashboard')]; + + const config = { + testFiles, + ...performanceConfig.getAll(), + }; + + const apmGlobalLabels = { + ...performanceConfig.get('kbnTestServer').env.ELASTIC_APM_GLOBAL_LABELS, + ftrConfig: `x-pack/test/performance/tests/journeys/flight_dashboard/config.ts`, + performancePhase: process.env.TEST_PERFORMANCE_PHASE, + journeyName: 'flight_dashboard', + }; + + return { + ...config, + kbnTestServer: { + ...config.kbnTestServer, + env: { + ...config.kbnTestServer.env, + ELASTIC_APM_GLOBAL_LABELS: serializeApmGlobalLabels(apmGlobalLabels), + }, + }, + }; +} diff --git a/x-pack/test/performance/tests/playwright/flight_dashboard.ts b/x-pack/test/performance/journeys/flight_dashboard/flight_dashboard.ts similarity index 97% rename from x-pack/test/performance/tests/playwright/flight_dashboard.ts rename to x-pack/test/performance/journeys/flight_dashboard/flight_dashboard.ts index 4844265018a05..344ae13511917 100644 --- a/x-pack/test/performance/tests/playwright/flight_dashboard.ts +++ b/x-pack/test/performance/journeys/flight_dashboard/flight_dashboard.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { StepCtx } from '../../services/performance'; -export default function flightDashboard({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { describe('flight_dashboard', () => { it('flight_dashboard', async () => { const performance = getService('performance'); diff --git a/x-pack/test/performance/journeys/login/config.ts b/x-pack/test/performance/journeys/login/config.ts new file mode 100644 index 0000000000000..230713eba3024 --- /dev/null +++ b/x-pack/test/performance/journeys/login/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrConfigProviderContext } from '@kbn/test'; +import { serializeApmGlobalLabels } from '../../utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const performanceConfig = await readConfigFile(require.resolve('../base.config')); + + const testFiles = [require.resolve('./login')]; + + const config = { + testFiles, + ...performanceConfig.getAll(), + }; + + const apmGlobalLabels = { + ...performanceConfig.get('kbnTestServer').env.ELASTIC_APM_GLOBAL_LABELS, + ftrConfig: `x-pack/test/performance/tests/journeys/login/config.ts`, + performancePhase: process.env.TEST_PERFORMANCE_PHASE, + journeyName: 'login', + }; + + return { + ...config, + kbnTestServer: { + ...config.kbnTestServer, + env: { + ...config.kbnTestServer.env, + ELASTIC_APM_GLOBAL_LABELS: serializeApmGlobalLabels(apmGlobalLabels), + }, + }, + }; +} diff --git a/x-pack/test/performance/tests/playwright/login.ts b/x-pack/test/performance/journeys/login/login.ts similarity index 94% rename from x-pack/test/performance/tests/playwright/login.ts rename to x-pack/test/performance/journeys/login/login.ts index 74baabc049f86..f1fd17174322f 100644 --- a/x-pack/test/performance/tests/playwright/login.ts +++ b/x-pack/test/performance/journeys/login/login.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { StepCtx } from '../../services/performance'; -export default function ecommerceDashboard({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { describe('login', () => { it('login', async () => { const inputDelays = getService('inputDelays'); diff --git a/x-pack/test/performance/journeys/many_fields_discover/config.ts b/x-pack/test/performance/journeys/many_fields_discover/config.ts new file mode 100644 index 0000000000000..85e4b26a18b9c --- /dev/null +++ b/x-pack/test/performance/journeys/many_fields_discover/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrConfigProviderContext } from '@kbn/test'; +import { serializeApmGlobalLabels } from '../../utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const performanceConfig = await readConfigFile(require.resolve('../base.config')); + + const testFiles = [require.resolve('./many_fields_discover')]; + + const config = { + testFiles, + ...performanceConfig.getAll(), + }; + + const apmGlobalLabels = { + ...performanceConfig.get('kbnTestServer').env.ELASTIC_APM_GLOBAL_LABELS, + ftrConfig: `x-pack/test/performance/tests/journeys/many_fields_discover/config.ts`, + performancePhase: process.env.TEST_PERFORMANCE_PHASE, + journeyName: 'many_fields_discover', + }; + + return { + ...config, + kbnTestServer: { + ...config.kbnTestServer, + env: { + ...config.kbnTestServer.env, + ELASTIC_APM_GLOBAL_LABELS: serializeApmGlobalLabels(apmGlobalLabels), + }, + }, + }; +} diff --git a/x-pack/test/performance/tests/playwright/many_fields_discover.ts b/x-pack/test/performance/journeys/many_fields_discover/many_fields_discover.ts similarity index 96% rename from x-pack/test/performance/tests/playwright/many_fields_discover.ts rename to x-pack/test/performance/journeys/many_fields_discover/many_fields_discover.ts index 0e66b63bb5794..381f0b4e46003 100644 --- a/x-pack/test/performance/tests/playwright/many_fields_discover.ts +++ b/x-pack/test/performance/journeys/many_fields_discover/many_fields_discover.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { StepCtx } from '../../services/performance'; -export default function manyFieldsDiscover({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { // FAILING: https://github.com/elastic/kibana/issues/130287 describe.skip('many_fields_discover', () => { const performance = getService('performance'); diff --git a/x-pack/test/performance/journeys/promotion_tracking_dashboard/config.ts b/x-pack/test/performance/journeys/promotion_tracking_dashboard/config.ts new file mode 100644 index 0000000000000..fb8d500aa7c09 --- /dev/null +++ b/x-pack/test/performance/journeys/promotion_tracking_dashboard/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrConfigProviderContext } from '@kbn/test'; +import { serializeApmGlobalLabels } from '../../utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const performanceConfig = await readConfigFile(require.resolve('../base.config')); + + const testFiles = [require.resolve('./promotion_tracking_dashboard')]; + + const config = { + testFiles, + ...performanceConfig.getAll(), + }; + + const apmGlobalLabels = { + ...performanceConfig.get('kbnTestServer').env.ELASTIC_APM_GLOBAL_LABELS, + ftrConfig: `x-pack/test/performance/tests/journeys/promotion_tracking_dashboard/config.ts`, + performancePhase: process.env.TEST_PERFORMANCE_PHASE, + journeyName: 'promotion_tracking_dashboard', + }; + + return { + ...config, + kbnTestServer: { + ...config.kbnTestServer, + env: { + ...config.kbnTestServer.env, + ELASTIC_APM_GLOBAL_LABELS: serializeApmGlobalLabels(apmGlobalLabels), + }, + }, + }; +} diff --git a/x-pack/test/performance/tests/playwright/promotion_tracking_dashboard.ts b/x-pack/test/performance/journeys/promotion_tracking_dashboard/promotion_tracking_dashboard.ts similarity index 97% rename from x-pack/test/performance/tests/playwright/promotion_tracking_dashboard.ts rename to x-pack/test/performance/journeys/promotion_tracking_dashboard/promotion_tracking_dashboard.ts index cb0fb09aafefa..7c44573962a1e 100644 --- a/x-pack/test/performance/tests/playwright/promotion_tracking_dashboard.ts +++ b/x-pack/test/performance/journeys/promotion_tracking_dashboard/promotion_tracking_dashboard.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { StepCtx } from '../../services/performance'; -export default function promotionTrackingDashboard({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { describe('promotion_tracking_dashboard', () => { const performance = getService('performance'); const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/performance/journeys/web_logs_dashboard/config.ts b/x-pack/test/performance/journeys/web_logs_dashboard/config.ts new file mode 100644 index 0000000000000..5e3bbd832b1b6 --- /dev/null +++ b/x-pack/test/performance/journeys/web_logs_dashboard/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrConfigProviderContext } from '@kbn/test'; +import { serializeApmGlobalLabels } from '../../utils'; + +export default async function webLogsDashboard({ readConfigFile, log }: FtrConfigProviderContext) { + const performanceConfig = await readConfigFile(require.resolve('../base.config')); + + const testFiles = [require.resolve('./web_logs_dashboard')]; + + const config = { + testFiles, + ...performanceConfig.getAll(), + }; + + const apmGlobalLabels = { + ...performanceConfig.get('kbnTestServer').env.ELASTIC_APM_GLOBAL_LABELS, + ftrConfig: `x-pack/test/performance/tests/journeys/web_logs_dashboard/config.ts`, + performancePhase: process.env.TEST_PERFORMANCE_PHASE, + journeyName: 'web_logs_dashboard', + }; + + return { + ...config, + kbnTestServer: { + ...config.kbnTestServer, + env: { + ...config.kbnTestServer.env, + ELASTIC_APM_GLOBAL_LABELS: serializeApmGlobalLabels(apmGlobalLabels), + }, + }, + }; +} diff --git a/x-pack/test/performance/tests/playwright/web_logs_dashboard.ts b/x-pack/test/performance/journeys/web_logs_dashboard/web_logs_dashboard.ts similarity index 96% rename from x-pack/test/performance/tests/playwright/web_logs_dashboard.ts rename to x-pack/test/performance/journeys/web_logs_dashboard/web_logs_dashboard.ts index 6ecee7f1244f7..6cb7b05df14d4 100644 --- a/x-pack/test/performance/tests/playwright/web_logs_dashboard.ts +++ b/x-pack/test/performance/journeys/web_logs_dashboard/web_logs_dashboard.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { StepCtx } from '../../services/performance'; -export default function weblogDashboard({ getService }: FtrProviderContext) { +export default function ({ getService }: FtrProviderContext) { describe('weblogs_dashboard', () => { it('weblogs_dashboard', async () => { const performance = getService('performance'); diff --git a/x-pack/test/performance/services/performance.ts b/x-pack/test/performance/services/performance.ts index ffe7211c63153..35db8020309a7 100644 --- a/x-pack/test/performance/services/performance.ts +++ b/x-pack/test/performance/services/performance.ts @@ -37,6 +37,10 @@ export class PerformanceTestingService extends FtrService { constructor(ctx: FtrProviderContext) { super(ctx); + + ctx.getService('lifecycle').cleanup.add(async () => { + await this.shutdownBrowser(); + }); } private getKibanaUrl() { @@ -173,7 +177,7 @@ export class PerformanceTestingService extends FtrService { } } - public async shutdownBrowser() { + private async shutdownBrowser() { if (this.browser) { await (await this.getBrowserInstance()).close(); } diff --git a/x-pack/test/performance/tests/playwright/index.ts b/x-pack/test/performance/tests/playwright/index.ts deleted file mode 100644 index d7e7c43b00178..0000000000000 --- a/x-pack/test/performance/tests/playwright/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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, getService }: FtrProviderContext) { - const performance = getService('performance'); - - describe('Performance tests', () => { - loadTestFile(require.resolve('./login')); - loadTestFile(require.resolve('./ecommerce_dashboard')); - loadTestFile(require.resolve('./flight_dashboard')); - loadTestFile(require.resolve('./web_logs_dashboard')); - loadTestFile(require.resolve('./promotion_tracking_dashboard')); - loadTestFile(require.resolve('./many_fields_discover')); - - after(async () => { - await performance.shutdownBrowser(); - }); - }); -} diff --git a/x-pack/test/performance/utils.ts b/x-pack/test/performance/utils.ts new file mode 100644 index 0000000000000..bb6f8e0aebe64 --- /dev/null +++ b/x-pack/test/performance/utils.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. + */ + +export function serializeApmGlobalLabels(obj: any) { + return Object.entries(obj) + .filter(([, v]) => !!v) + .reduce((acc, [k, v]) => (acc ? `${acc},${k}=${v}` : `${k}=${v}`), ''); +} 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 5578ee1ea3f72..1bd158019c6f4 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/reporting_api_integration/reporting_and_security/__snapshots__/generate_csv_discover.snap b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/generate_csv_discover.snap index b20a98a5287dd..928be4d7e85e1 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/generate_csv_discover.snap +++ b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/generate_csv_discover.snap @@ -8,3 +8,24 @@ bwMtOW0BH63Xcmy432HJ,ecommerce,\\"-\\",\\"Men's Clothing, Men's Shoes\\",\\"Men' 5AMtOW0BH63Xcmy432HJ,ecommerce,\\"-\\",\\"Men's Clothing\\",\\"Men's Clothing\\",EUR,Kamal,Kamal,\\"Kamal Salazar\\",\\"Kamal Salazar\\",MALE,39,Salazar,Salazar,\\"(empty)\\",Tuesday,1,\\"kamal@salazar-family.zzz\\",Istanbul,Asia,TR,\\"POINT (29 41)\\",Istanbul,\\"Spherecords, Spritechnologies\\",\\"Spherecords, Spritechnologies\\",\\"Jun 24, 2019 @ 00:00:00.000\\",567736,\\"sold_product_567736_24718, sold_product_567736_24306\\",\\"sold_product_567736_24718, sold_product_567736_24306\\",\\"11.992, 75\\",\\"11.992, 75\\",\\"Men's Clothing, Men's Clothing\\",\\"Men's Clothing, Men's Clothing\\",\\"Dec 13, 2016 @ 00:00:00.000, Dec 13, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Spherecords, Spritechnologies\\",\\"Spherecords, Spritechnologies\\",\\"6.109, 36.75\\",\\"11.992, 75\\",\\"24,718, 24,306\\",\\"Pyjama bottoms - light grey multicolor, Waterproof trousers - scarlet\\",\\"Pyjama bottoms - light grey multicolor, Waterproof trousers - scarlet\\",\\"1, 1\\",\\"ZO0663706637, ZO0620906209\\",\\"0, 0\\",\\"11.992, 75\\",\\"11.992, 75\\",\\"0, 0\\",\\"ZO0663706637, ZO0620906209\\",87,87,2,2,order,kamal " `; + +exports[`Reporting APIs Generate CSV from SearchSource with unmapped fields includes all unmapped fields to the report 1`] = ` +"\\"_id\\",\\"_index\\",\\"_score\\",\\"nested.unmapped\\",text,unmapped +1,recipes,\\"-\\",\\"-\\",text1,unmapped1 +2,recipes,\\"-\\",unmapped2,text2,\\"-\\" +" +`; + +exports[`Reporting APIs Generate CSV from SearchSource with unmapped fields includes an unmapped field to the report 1`] = ` +"\\"_id\\",\\"_index\\",\\"_score\\",text,unmapped +1,recipes,\\"-\\",text1,unmapped1 +2,recipes,\\"-\\",text2,\\"-\\" +" +`; + +exports[`Reporting APIs Generate CSV from SearchSource with unmapped fields includes an unmapped nested field to the report 1`] = ` +"\\"_id\\",\\"_index\\",\\"_score\\",\\"nested.unmapped\\",text +1,recipes,\\"-\\",\\"-\\",text1 +2,recipes,\\"-\\",unmapped2,text2 +" +`; diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/generate_csv_discover.ts b/x-pack/test/reporting_api_integration/reporting_and_security/generate_csv_discover.ts index 0490cddb568d2..d7f9d772f826d 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/generate_csv_discover.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/generate_csv_discover.ts @@ -12,6 +12,8 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const reportingAPI = getService('reportingAPI'); describe('Generate CSV from SearchSource', () => { @@ -71,5 +73,63 @@ export default function ({ getService }: FtrProviderContext) { await reportingAPI.teardownEcommerce(); await reportingAPI.deleteAllReports(); }); + + describe('with unmapped fields', () => { + before(async () => { + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/reporting/unmapped_fields' + ); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/reporting/unmapped_fields.json' + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/reporting/unmapped_fields'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/reporting/unmapped_fields.json' + ); + }); + + async function generateCsvReport(fields: string[]) { + const { text } = await reportingAPI.generateCsv({ + title: 'CSV Report', + browserTimezone: 'UTC', + objectType: 'search', + version: '7.15.0', + searchSource: { + version: true, + query: { query: '', language: 'kuery' }, + index: '5c620ea0-dc4f-11ec-972a-bf98ce1eebd7', + sort: [{ order_date: 'desc' }], + fields: fields.map((field) => ({ field, include_unmapped: 'true' })), + filter: [], + } as SerializedSearchSourceFields, + }); + + const { path } = JSON.parse(text) as { path: string }; + await reportingAPI.waitForJobToFinish(path); + + return reportingAPI.getCompletedJobOutput(path); + } + + it('includes an unmapped field to the report', async () => { + const csvFile = await generateCsvReport(['text', 'unmapped']); + + expectSnapshot(csvFile).toMatch(); + }); + + it('includes an unmapped nested field to the report', async () => { + const csvFile = await generateCsvReport(['text', 'nested.unmapped']); + + expectSnapshot(csvFile).toMatch(); + }); + + it('includes all unmapped fields to the report', async () => { + const csvFile = await generateCsvReport(['*']); + + expectSnapshot(csvFile).toMatch(); + }); + }); }); } diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index 808c813145b84..b2d529ef8a358 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -28,5 +28,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./usage')); loadTestFile(require.resolve('./ilm_migration_apis')); loadTestFile(require.resolve('./error_codes')); + loadTestFile(require.resolve('./validation')); }); } diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts b/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts index ad086319776f9..485cbb7179237 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts @@ -67,7 +67,7 @@ export default function ({ getService }: FtrProviderContext) { { browserTimezone: 'UTC', title: 'test PDF disallowed', - layout: { id: 'preserve' }, + layout: { id: 'preserve_layout' }, relativeUrls: ['/fooyou'], objectType: 'dashboard', version: '7.14.0', @@ -83,7 +83,7 @@ export default function ({ getService }: FtrProviderContext) { { browserTimezone: 'UTC', title: 'test PDF allowed', - layout: { id: 'preserve' }, + layout: { id: 'preserve_layout' }, relativeUrls: ['/fooyou'], objectType: 'dashboard', version: '7.14.0', @@ -101,7 +101,7 @@ export default function ({ getService }: FtrProviderContext) { { browserTimezone: 'UTC', title: 'test PDF disallowed', - layout: { id: 'preserve' }, + layout: { id: 'preserve_layout' }, relativeUrls: ['/fooyou'], objectType: 'visualization', version: '7.14.0', @@ -117,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) { { browserTimezone: 'UTC', title: 'test PDF allowed', - layout: { id: 'preserve' }, + layout: { id: 'preserve_layout' }, relativeUrls: ['/fooyou'], objectType: 'visualization', version: '7.14.0', @@ -135,7 +135,7 @@ export default function ({ getService }: FtrProviderContext) { { browserTimezone: 'UTC', title: 'test PDF disallowed', - layout: { id: 'preserve' }, + layout: { id: 'preserve_layout' }, relativeUrls: ['/fooyou'], objectType: 'canvas', version: '7.14.0', @@ -151,7 +151,7 @@ export default function ({ getService }: FtrProviderContext) { { browserTimezone: 'UTC', title: 'test PDF allowed', - layout: { id: 'preserve' }, + layout: { id: 'preserve_layout' }, relativeUrls: ['/fooyou'], objectType: 'canvas', version: '7.14.0', diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/validation.ts b/x-pack/test/reporting_api_integration/reporting_and_security/validation.ts new file mode 100644 index 0000000000000..e955c123d24d0 --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_and_security/validation.ts @@ -0,0 +1,142 @@ +/* + * 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 supertest from 'supertest'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const reportingAPI = getService('reportingAPI'); + const retry = getService('retry'); + const log = getService('log'); + const supertestSvc = getService('supertest'); + + const status = (downloadReportPath: string, response: supertest.Response) => { + if (response.status === 503) { + log.debug(`Report at path ${downloadReportPath} is pending`); + } else if (response.status === 200) { + log.debug(`Report at path ${downloadReportPath} is complete`); + } else { + log.debug(`Report at path ${downloadReportPath} returned code ${response.status}`); + } + }; + + describe('Job parameter validation', () => { + before(async () => { + await reportingAPI.initEcommerce(); + }); + after(async () => { + await reportingAPI.teardownEcommerce(); + await reportingAPI.deleteAllReports(); + }); + + describe('printablePdfV2', () => { + it('allows width and height to have decimal', async () => { + const downloadReportPath = await reportingAPI.postJobJSON( + '/api/reporting/generate/printablePdfV2', + { jobParams: createPdfV2Params(1541.5999755859375) } + ); + + await retry.tryForTime(18000, async () => { + const response: supertest.Response = await supertestSvc + .get(downloadReportPath) + .responseType('blob') + .set('kbn-xsrf', 'xxx'); + status(downloadReportPath, response); + + expect(response.status).equal(200); + }); + }); + + it('fails if width or height are non-numeric', async () => { + const downloadReportPath = await reportingAPI.postJobJSON( + '/api/reporting/generate/printablePdfV2', + { jobParams: createPdfV2Params('cucucachoo') } + ); + await retry.tryForTime(18000, async () => { + const response: supertest.Response = await supertestSvc + .get(downloadReportPath) + .responseType('blob') + .set('kbn-xsrf', 'xxx'); + + expect(response.status).equal(500); + }); + }); + + it('fails if there is an invalid layout ID', async () => { + const downloadReportPath = await reportingAPI.postJobJSON( + '/api/reporting/generate/printablePdfV2', + { jobParams: createPdfV2Params(1541, 'landscape') } + ); + await retry.tryForTime(18000, async () => { + const response: supertest.Response = await supertestSvc + .get(downloadReportPath) + .responseType('blob') + .set('kbn-xsrf', 'xxx'); + + expect(response.status).equal(500); + }); + }); + }); + + describe('pngV2', () => { + it('fails if width or height are non-numeric', async () => { + const downloadReportPath = await reportingAPI.postJobJSON('/api/reporting/generate/pngV2', { + jobParams: createPngV2Params('cucucachoo'), + }); + await retry.tryForTime(18000, async () => { + const response: supertest.Response = await supertestSvc + .get(downloadReportPath) + .responseType('blob') + .set('kbn-xsrf', 'xxx'); + + expect(response.status).equal(500); + }); + }); + }); + }); +} + +const createPdfV2Params = (testWidth: number | string, layoutId = 'preserve_layout') => + `(browserTimezone:UTC,layout:` + + `(dimensions:(height:1492,width:${testWidth}),id:${layoutId}),` + + `locatorParams:\u0021((id:DASHBOARD_APP_LOCATOR,params:` + + `(dashboardId:\'6c263e00-1c6d-11ea-a100-8589bb9d7c6b\',` + + `preserveSavedFilters:\u0021t,` + + `timeRange:(from:\'2019-03-23T03:06:17.785Z\',to:\'2019-10-04T02:33:16.708Z\'),` + + `useHash:\u0021f,` + + `viewMode:view),` + + `version:\'8.2.0\')),` + + `objectType:dashboard,` + + `title:\'Ecom Dashboard\',` + + `version:\'8.2.0\')`; + +const createPngV2Params = (testWidth: number | string) => + `(browserTimezone:UTC,layout:` + + `(dimensions:(height:648,width:${testWidth}),id:preserve_layout),` + + `locatorParams:(id:VISUALIZE_APP_LOCATOR,params:` + + `(filters:\u0021(),` + + `indexPattern:\'5193f870-d861-11e9-a311-0fa548c5f953\',` + + `linked:\u0021t,` + + `query:(language:kuery,query:\'\'),` + + `savedSearchId:\'6091ead0-1c6d-11ea-a100-8589bb9d7c6b\',` + + `timeRange:(from:\'2019-03-23T03:06:17.785Z\',to:\'2019-10-04T02:33:16.708Z\'),` + + `uiState:(),` + + `vis:(aggs:\u0021((enabled:\u0021t,id:\'1\',params:(emptyAsNull:\u0021f),schema:metric,type:count),` + + `(enabled:\u0021t,` + + `id:\'2\',` + + `params:(field:customer_first_name.keyword,missingBucket:\u0021f,missingBucketLabel:Missing,order:desc,orderBy:\'1\',otherBucket:\u0021f,otherBucketLabel:Other,size:10),` + + `schema:segment,type:terms)),` + + `params:(maxFontSize:72,minFontSize:18,orientation:single,palette:(name:kibana_palette,type:palette),scale:linear,showLabel:\u0021t),` + + `title:\'Tag Cloud of Names\',` + + `type:tagcloud),` + + `visId:\'1bba55f0-507e-11eb-9c0d-97106882b997\'),` + + `version:\'8.2.0\'),` + + `objectType:visualization,` + + `title:\'Tag Cloud of Names\',` + + `version:\'8.2.0\')`; 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 840c36a558ba0..ea05bc19b6dd1 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 () => { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 8c6a1cb88c0ba..e7684ba8b7905 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -45,6 +45,7 @@ { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "../../src/plugins/telemetry/tsconfig.json" }, { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../plugins/actions/tsconfig.json" }, @@ -93,7 +94,6 @@ { "path": "../plugins/timelines/tsconfig.json" }, { "path": "../plugins/transform/tsconfig.json" }, { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, - { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, { "path": "../plugins/watcher/tsconfig.json" }, { "path": "../plugins/rollup/tsconfig.json" }, diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index 40ae1d5624776..be61b9cf5e5a6 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -63,7 +63,6 @@ export default function catalogueTests({ getService }: FtrProviderContext) { const exceptions = [ 'monitoring', 'enterpriseSearch', - 'enterpriseSearchContent', 'elasticsearch', 'appSearch', 'workplaceSearch', diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 74f1150965c5e..10e5cc1c37cde 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -53,7 +53,6 @@ export default function navLinksTests({ getService }: FtrProviderContext) { navLinksBuilder.except( 'monitoring', 'enterpriseSearch', - 'enterpriseSearchContent', 'appSearch', 'workplaceSearch' ) diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts index f1dffc33560c5..0e773845508da 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -27,7 +27,6 @@ export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesExceptions = [ // enterprise_search plugin is loaded but disabled because security isn't enabled in ES. That means the following 4 capabilities are disabled 'enterpriseSearch', - 'enterpriseSearchContent', 'elasticsearch', 'appSearch', 'workplaceSearch', diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts index 6ac59638dac2e..222a0a7ae215e 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts @@ -19,7 +19,6 @@ export default function navLinksTests({ getService }: FtrProviderContext) { const uiCapabilitiesExceptions = [ // enterprise_search plugin is loaded but disabled because security isn't enabled in ES. That means the following 4 capabilities are disabled 'enterpriseSearch', - 'enterpriseSearchContent', 'appSearch', 'workplaceSearch', ]; diff --git a/yarn.lock b/yarn.lock index 4bf463ee5a379..eb17d605100a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1483,10 +1483,10 @@ "@elastic/transport" "^8.0.2" tslib "^2.3.0" -"@elastic/ems-client@8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-8.3.2.tgz#a12eafcfd9ac8d3068da78a5a77503ea8a89f67c" - integrity sha512-81u+Z7+4Y2Fu+sTl9QOKdG3SVeCzzpfyCsHFR8X0V2WFCpQa+SU4sSN9WhdLHz/pe9oi6Gtt5eFMF90TOO/ckg== +"@elastic/ems-client@8.3.3": + version "8.3.3" + resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-8.3.3.tgz#16ddd582e1029055a5a77e7bebbc5b57b3246ff7" + integrity sha512-vcgPwnAw7QjcR68IddErobZqwJdH0T4h/6U4swUR3XEF+9jojNZnxBkvM3mEV9Z7QLQxDprebJJu04d37lzlUw== dependencies: "@types/geojson" "^7946.0.7" "@types/lru-cache" "^5.1.0" @@ -3004,6 +3004,22 @@ version "0.0.0" uid "" +"@kbn/core-injected-metadata-browser-internal@link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser-internal": + version "0.0.0" + uid "" + +"@kbn/core-injected-metadata-browser-mocks@link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser-mocks": + version "0.0.0" + uid "" + +"@kbn/core-injected-metadata-browser@link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser": + version "0.0.0" + uid "" + +"@kbn/core-injected-metadata-common-internal@link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-common-internal": + version "0.0.0" + uid "" + "@kbn/crypto@link:bazel-bin/packages/kbn-crypto": version "0.0.0" uid "" @@ -3336,6 +3352,10 @@ version "0.0.0" uid "" +"@kbn/utility-types-jest@link:bazel-bin/packages/kbn-utility-types-jest": + version "0.0.0" + uid "" + "@kbn/utility-types@link:bazel-bin/packages/kbn-utility-types": version "0.0.0" uid "" @@ -6282,6 +6302,22 @@ version "0.0.0" uid "" +"@types/kbn__core-injected-metadata-browser-internal@link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser-internal/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__core-injected-metadata-browser-mocks@link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser-mocks/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__core-injected-metadata-browser@link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-browser/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__core-injected-metadata-common-internal@link:bazel-bin/packages/core/injected-metadata/core-injected-metadata-common-internal/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__core-public-internal-base@link:bazel-bin/packages/core/public/internal-base/npm_module_types": version "0.0.0" uid "" @@ -6570,6 +6606,10 @@ version "0.0.0" uid "" +"@types/kbn__utility-types-jest@link:bazel-bin/packages/kbn-utility-types-jest/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__utility-types@link:bazel-bin/packages/kbn-utility-types/npm_module_types": version "0.0.0" uid "" @@ -7191,10 +7231,10 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.0.tgz#b23ba7e7f4f59069529c57f0cbb7f5fba74affe7" - integrity sha512-ehqwZemosqiWVe+W0f5GqcLH7NgtjMBmcknmeaPG6YZHc7EZ69XbD7VVNZcT/L8lyMIL/KG99MsGcvDuFWo3Yw== +"@types/selenium-webdriver@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.1.tgz#aefb038f0462fd880f9c9581b8b3b71ce385719c" + integrity sha512-NxxZZek50ylIACiXebKQYHD3D4One3WXOasEXWazL6aTfYbZob7ClNKxUpg8I4/oWArX87oPWvj1cHKqfel3Hg== dependencies: "@types/ws" "*" @@ -25620,10 +25660,10 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selenium-webdriver@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.1.2.tgz#d463b4335632d2ea41a9e988e435a55dc41f5314" - integrity sha512-e4Ap8vQvhipgBB8Ry9zBiKGkU6kHKyNnWiavGGLKkrdW81Zv7NVMtFOL/j3yX0G8QScM7XIXijKssNd4EUxSOw== +selenium-webdriver@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.2.0.tgz#d3c9704735c6228e09580eb4613932b30bdb4d27" + integrity sha512-gPPXYSz4jJBM2kANRQ9cZW6KFBzR/ptxqGLtyC75eXtdgOsWWRRRyZz5F2pqdnwNmAjrCSFMMXfisJaZeWVejg== dependencies: jszip "^3.6.0" tmp "^0.2.1"