diff --git a/.buildkite/scripts/build_kibana.sh b/.buildkite/scripts/build_kibana.sh index 482f730284a94..d450e988799bc 100755 --- a/.buildkite/scripts/build_kibana.sh +++ b/.buildkite/scripts/build_kibana.sh @@ -5,9 +5,9 @@ set -euo pipefail export KBN_NP_PLUGINS_BUILT=true echo "--- Build Kibana Distribution" -if [[ "${GITHUB_PR_LABELS:-}" == *"ci:build-all-platforms"* ]]; then +if is_pr_with_label "ci:build-all-platforms"; then node scripts/build --all-platforms --skip-os-packages -elif [[ "${GITHUB_PR_LABELS:-}" == *"ci:build-os-packages"* ]]; then +elif is_pr_with_label "ci:build-os-packages"; then node scripts/build --all-platforms --docker-cross-compile else node scripts/build diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index 82c42af67f226..b8b9ef2ffb7de 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -41,7 +41,7 @@ export ELASTIC_APM_SERVER_URL=https://kibana-ci-apm.apm.us-central1.gcp.cloud.es export ELASTIC_APM_SECRET_TOKEN=7YKhoXsO4MzjhXjx2c if is_pr; then - if [[ "${GITHUB_PR_LABELS:-}" == *"ci:collect-apm"* ]]; then + if is_pr_with_label "ci:collect-apm"; then export ELASTIC_APM_ACTIVE=true export ELASTIC_APM_CONTEXT_PROPAGATION_ONLY=false else diff --git a/.buildkite/scripts/common/util.sh b/.buildkite/scripts/common/util.sh index 18fa1b1d79000..293eb8e6b8d27 100755 --- a/.buildkite/scripts/common/util.sh +++ b/.buildkite/scripts/common/util.sh @@ -14,6 +14,25 @@ is_pr() { false } +is_pr_with_label() { + match="$1" + + IFS=',' read -ra labels <<< "${GITHUB_PR_LABELS:-}" + + for label in "${labels[@]}" + do + if [ "$label" == "$match" ]; then + return + fi + done + + false +} + +is_auto_commit_disabled() { + is_pr_with_label "ci:no-auto-commit" +} + check_for_changed_files() { RED='\033[0;31m' YELLOW='\033[0;33m' @@ -23,7 +42,7 @@ check_for_changed_files() { GIT_CHANGES="$(git ls-files --modified -- . ':!:.bazelrc')" if [ "$GIT_CHANGES" ]; then - if [[ "$SHOULD_AUTO_COMMIT_CHANGES" == "true" && "${BUILDKITE_PULL_REQUEST:-}" ]]; then + if ! is_auto_commit_disabled && [[ "$SHOULD_AUTO_COMMIT_CHANGES" == "true" && "${BUILDKITE_PULL_REQUEST:-}" ]]; then NEW_COMMIT_MESSAGE="[CI] Auto-commit changed files from '$1'" PREVIOUS_COMMIT_MESSAGE="$(git log -1 --pretty=%B)" diff --git a/.buildkite/scripts/steps/artifacts/build.sh b/.buildkite/scripts/steps/artifacts/build.sh index 936c59739a669..337d0289daa72 100644 --- a/.buildkite/scripts/steps/artifacts/build.sh +++ b/.buildkite/scripts/steps/artifacts/build.sh @@ -25,7 +25,9 @@ buildkite-agent artifact upload "dependencies-$FULL_VERSION.csv.sha512.txt" buildkite-agent artifact upload 'i18n/*.json' cd - -cd .beats -buildkite-agent artifact upload 'metricbeat-*' -buildkite-agent artifact upload 'filebeat-*' -cd - +if [ -d .beats ]; then + cd .beats + buildkite-agent artifact upload 'metricbeat-*' + buildkite-agent artifact upload 'filebeat-*' + cd - +fi diff --git a/.buildkite/scripts/steps/checks.sh b/.buildkite/scripts/steps/checks.sh index 024037a8a4bb9..8388dc82f5254 100755 --- a/.buildkite/scripts/steps/checks.sh +++ b/.buildkite/scripts/steps/checks.sh @@ -5,7 +5,7 @@ set -euo pipefail export DISABLE_BOOTSTRAP_VALIDATION=false .buildkite/scripts/bootstrap.sh -.buildkite/scripts/steps/checks/commit/commit.sh +.buildkite/scripts/steps/checks/precommit_hook.sh .buildkite/scripts/steps/checks/bazel_packages.sh .buildkite/scripts/steps/checks/telemetry.sh .buildkite/scripts/steps/checks/ts_projects.sh diff --git a/.buildkite/scripts/steps/checks/commit/commit.sh b/.buildkite/scripts/steps/checks/commit/commit.sh deleted file mode 100755 index 5ff2632103a63..0000000000000 --- a/.buildkite/scripts/steps/checks/commit/commit.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh - -# Runs pre-commit hook script for the files touched in the last commit. -# That way we can ensure a set of quick commit checks earlier as we removed -# the pre-commit hook installation by default. -# If files are more than 200 we will skip it and just use -# the further ci steps that already check linting and file casing for the entire repo. -echo --- Quick commit checks -checks-reporter-with-killswitch "Quick commit checks" \ - "$(dirname "${0}")/commit_check_runner.sh" diff --git a/.buildkite/scripts/steps/checks/commit/commit_check_runner.sh b/.buildkite/scripts/steps/checks/commit/commit_check_runner.sh deleted file mode 100755 index 8d35c3698f3e1..0000000000000 --- a/.buildkite/scripts/steps/checks/commit/commit_check_runner.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -run_quick_commit_checks() { - echo "!!!!!!!! ATTENTION !!!!!!!! -That check is intended to provide earlier CI feedback after we remove the automatic install for the local pre-commit hook. -If you want, you can still manually install the pre-commit hook locally by running 'node scripts/register_git_hook locally' -!!!!!!!!!!!!!!!!!!!!!!!!!!! -" - - node scripts/precommit_hook.js --ref HEAD~1..HEAD --max-files 200 --verbose -} - -run_quick_commit_checks diff --git a/.buildkite/scripts/steps/checks/precommit_hook.sh b/.buildkite/scripts/steps/checks/precommit_hook.sh new file mode 100755 index 0000000000000..8fa51a4f4d23c --- /dev/null +++ b/.buildkite/scripts/steps/checks/precommit_hook.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +# Runs pre-commit hook script for the files touched in the last commit. +# That way we can ensure a set of quick commit checks earlier as we removed +# the pre-commit hook installation by default. +# If files are more than 200 we will skip it and just use +# the further ci steps that already check linting and file casing for the entire repo. +echo --- Run Precommit Hook + +echo "!!!!!!!! ATTENTION !!!!!!!! +That check is intended to provide earlier CI feedback after we remove the automatic install for the local pre-commit hook. +If you want, you can still manually install the pre-commit hook locally by running 'node scripts/register_git_hook locally' +!!!!!!!!!!!!!!!!!!!!!!!!!!!" + +node scripts/precommit_hook.js \ + --ref HEAD~1..HEAD \ + --max-files 200 \ + --verbose \ + --fix \ + --no-stage # we have to disable staging or check_for_changed_files won't see the changes + +check_for_changed_files 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' true diff --git a/.buildkite/scripts/steps/lint.sh b/.buildkite/scripts/steps/lint.sh index e94e7be4c7db2..ad61fced12f50 100755 --- a/.buildkite/scripts/steps/lint.sh +++ b/.buildkite/scripts/steps/lint.sh @@ -11,20 +11,26 @@ checks-reporter-with-killswitch "Lint: stylelint" \ node scripts/stylelint echo "stylelint ✅" +echo '--- Lint: eslint' # disable "Exit immediately" mode so that we can run eslint, capture it's exit code, and respond appropriately # after possibly commiting fixed files to the repo set +e; - -echo '--- Lint: eslint' -checks-reporter-with-killswitch "Lint: eslint" \ +if is_pr && ! is_auto_commit_disabled; then node scripts/eslint --no-cache --fix +else + node scripts/eslint --no-cache +fi eslint_exit=$? - # re-enable "Exit immediately" mode set -e; -check_for_changed_files 'node scripts/eslint --no-cache --fix' true +desc="node scripts/eslint --no-cache" +if is_pr && ! is_auto_commit_disabled; then + desc="$desc --fix" +fi + +check_for_changed_files "$desc" true if [[ "${eslint_exit}" != "0" ]]; then exit 1 diff --git a/.buildkite/scripts/steps/package_testing/test.sh b/.buildkite/scripts/steps/package_testing/test.sh index 390adc2dbacee..86e7bf8138875 100755 --- a/.buildkite/scripts/steps/package_testing/test.sh +++ b/.buildkite/scripts/steps/package_testing/test.sh @@ -41,9 +41,11 @@ trap "echoKibanaLogs" EXIT vagrant provision "$TEST_PACKAGE" -# export TEST_BROWSER_HEADLESS=1 -# export TEST_KIBANA_URL="http://elastic:changeme@$KIBANA_IP_ADDRESS:5601" -# export TEST_ES_URL=http://elastic:changeme@192.168.56.1:9200 +export TEST_BROWSER_HEADLESS=1 +export TEST_KIBANA_URL="http://elastic:changeme@$KIBANA_IP_ADDRESS:5601" +export TEST_ES_URL="http://elastic:changeme@192.168.56.1:9200" -# cd x-pack -# node scripts/functional_test_runner.js --include-tag=smoke +cd x-pack + +echo "--- FTR - Reporting" +node scripts/functional_test_runner.js --config test/functional/apps/visualize/config.ts --include-tag=smoke --quiet diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 450daeebd24d1..abd63289e0480 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -187,10 +187,11 @@ /x-pack/test/screenshot_creation/apps/ml_docs @elastic/ml-ui /x-pack/test/screenshot_creation/services/ml_screenshots.ts @elastic/ml-ui -# ML team owns and maintains the transform plugin despite it living in the Data management section. -/x-pack/plugins/transform/ @elastic/ml-ui +# Additional plugins maintained by the ML team. +/x-pack/plugins/aiops/ @elastic/ml-ui /x-pack/plugins/data_visualizer/ @elastic/ml-ui /x-pack/plugins/file_upload/ @elastic/ml-ui +/x-pack/plugins/transform/ @elastic/ml-ui /x-pack/test/accessibility/apps/transform.ts @elastic/ml-ui /x-pack/test/api_integration/apis/transform/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui diff --git a/docs/api/actions-and-connectors.asciidoc b/docs/api/actions-and-connectors.asciidoc index ff4cb8401091e..f8d286e00b856 100644 --- a/docs/api/actions-and-connectors.asciidoc +++ b/docs/api/actions-and-connectors.asciidoc @@ -23,13 +23,13 @@ For deprecated APIs, refer to <>. For information about the actions and connectors that {kib} supports, refer to <>. -include::actions-and-connectors/get.asciidoc[] -include::actions-and-connectors/get_all.asciidoc[] +include::actions-and-connectors/create.asciidoc[leveloffset=+1] +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/create.asciidoc[] include::actions-and-connectors/update.asciidoc[] include::actions-and-connectors/execute.asciidoc[] -include::actions-and-connectors/delete.asciidoc[] include::actions-and-connectors/legacy/index.asciidoc[] include::actions-and-connectors/legacy/get.asciidoc[] include::actions-and-connectors/legacy/get_all.asciidoc[] diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc index 401f4c5372688..d5208b9debfe9 100644 --- a/docs/api/actions-and-connectors/create.asciidoc +++ b/docs/api/actions-and-connectors/create.asciidoc @@ -1,26 +1,37 @@ [[create-connector-api]] -=== Create connector API +== Create connector API ++++ Create connector ++++ Creates a connector. +[discrete] [[create-connector-api-request]] -==== Request +=== {api-request-title} `POST :/api/actions/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]] -==== 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. + (Optional, string) An identifier for the space. If `space_id` is not provided + in the URL, the default space is used. +[discrete] [[create-connector-api-request-body]] -==== Request body +=== {api-request-body-title} `name`:: (Required, string) The display name for the connector. @@ -38,25 +49,27 @@ Creates a connector. + WARNING: Remember these values. You must provide them each time you call the <> API. +[discrete] [[create-connector-api-request-codes]] -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. +[discrete] [[create-connector-api-example]] -==== Example +=== {api-examples-title} [source,sh] -------------------------------------------------- -$ curl -X POST api/actions/connector -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +POST api/actions/connector { "name": "my-connector", "connector_type_id": ".index", "config": { "index": "test-index" } -}' +} -------------------------------------------------- // KIBANA diff --git a/docs/api/actions-and-connectors/delete.asciidoc b/docs/api/actions-and-connectors/delete.asciidoc index 021a3f7cdf3f7..1ef917f58d24f 100644 --- a/docs/api/actions-and-connectors/delete.asciidoc +++ b/docs/api/actions-and-connectors/delete.asciidoc @@ -1,5 +1,5 @@ [[delete-connector-api]] -=== Delete connector API +== Delete connector API ++++ Delete connector ++++ @@ -8,15 +8,24 @@ Deletes an connector by ID. WARNING: When you delete a connector, _it cannot be recovered_. +[discrete] [[delete-connector-api-request]] -==== Request +=== {api-request-title} `DELETE :/api/actions/connector/` `DELETE :/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] [[delete-connector-api-path-params]] -==== Path parameters +=== {api-path-parms-title} `id`:: (Required, string) The ID of the connector. @@ -24,16 +33,18 @@ WARNING: When you delete a connector, _it cannot be recovered_. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +[discrete] [[delete-connector-api-response-codes]] -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -==== Example +[discrete] +=== {api-examples-title} [source,sh] -------------------------------------------------- -$ curl -X DELETE api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad +DELETE api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -------------------------------------------------- // KIBANA diff --git a/docs/api/actions-and-connectors/get.asciidoc b/docs/api/actions-and-connectors/get.asciidoc index bc6b5fa8f364c..2d5cc4edd4276 100644 --- a/docs/api/actions-and-connectors/get.asciidoc +++ b/docs/api/actions-and-connectors/get.asciidoc @@ -1,20 +1,29 @@ [[get-connector-api]] -=== Get connector API +== Get connector API ++++ Get connector ++++ Retrieves a connector by ID. +[discrete] [[get-connector-api-request]] -==== Request +=== {api-request-title} `GET :/api/actions/connector/` `GET :/s//api/actions/connector/` +[discrete] +=== {api-prereq-title} + +You must have `read` privileges for the *Actions and Connectors* feature in the +*Management* section of the +<>. + +[discrete] [[get-connector-api-params]] -==== Path parameters +=== {api-path-parms-title} `id`:: (Required, string) The ID of the connector. @@ -22,18 +31,20 @@ Retrieves a connector by ID. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +[discrete] [[get-connector-api-codes]] -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. +[discrete] [[get-connector-api-example]] -==== Example +=== {api-examples-title} [source,sh] -------------------------------------------------- -$ curl -X GET api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad +GET api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -------------------------------------------------- // KIBANA @@ -54,4 +65,4 @@ The API returns the following: "is_deprecated": false, "is_missing_secrets": false } --------------------------------------------------- +-------------------------------------------------- \ No newline at end of file diff --git a/docs/api/actions-and-connectors/get_all.asciidoc b/docs/api/actions-and-connectors/get_all.asciidoc index 26bb7247e2ce1..b2ebe316fc5b2 100644 --- a/docs/api/actions-and-connectors/get_all.asciidoc +++ b/docs/api/actions-and-connectors/get_all.asciidoc @@ -1,36 +1,47 @@ [[get-all-connectors-api]] -=== Get all connectors API +== Get all connectors API ++++ Get all connectors ++++ Retrieves all connectors. +[discrete] [[get-all-connectors-api-request]] -==== Request +=== {api-request-title} `GET :/api/actions/connectors` `GET :/s//api/actions/connectors` +[discrete] +=== {api-prereq-title} + +You must have `read` privileges for the *Actions and Connectors* feature in the +*Management* section of the +<>. + +[discrete] [[get-all-connectors-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. +[discrete] [[get-all-connectors-api-codes]] -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. +[discrete] [[get-all-connectors-api-example]] -==== Example +=== {api-examples-title} [source,sh] -------------------------------------------------- -$ curl -X GET api/actions/connectors +GET api/actions/connectors -------------------------------------------------- // KIBANA @@ -64,4 +75,4 @@ The API returns the following: ] -------------------------------------------------- -<1> `referenced_by_count` - The number of saved-objects referencing this connector. This value is not calculated if `is_preconfigured: true`. \ No newline at end of file +<1> `referenced_by_count` indicates the number of saved objects that reference the connector. This value is not calculated if `is_preconfigured` is `true`. \ No newline at end of file diff --git a/docs/api/alerting/find_rules.asciidoc b/docs/api/alerting/find_rules.asciidoc index 2df8b3522725c..48c4bb25e0eea 100644 --- a/docs/api/alerting/find_rules.asciidoc +++ b/docs/api/alerting/find_rules.asciidoc @@ -43,7 +43,7 @@ NOTE: Rule `params` are stored as a {ref}/flattened.html[flattened field type] a (Optional, array|string) The fields to perform the `simple_query_string` parsed query against. `fields`:: - (Optional, array|string) The fields to return in the `attributes` key of the response. + (Optional, array of strings) The fields to return in the `attributes` key of the response. `sort_field`:: (Optional, string) Sorts the response. Could be a rule field returned in the `attributes` key of the response. diff --git a/docs/dev-tools/console/console.asciidoc b/docs/dev-tools/console/console.asciidoc index 21334c31011f4..69f81d838c143 100644 --- a/docs/dev-tools/console/console.asciidoc +++ b/docs/dev-tools/console/console.asciidoc @@ -12,7 +12,7 @@ To get started, open the main menu, click *Dev Tools*, then click *Console*. [role="screenshot"] image::dev-tools/console/images/console.png["Console"] -NOTE: You cannot to interact with the REST API of {kib} with the Console. +NOTE: **Console** supports only Elasticsearch APIs. You are unable to interact with the {kib} APIs with **Console** and must use curl or another HTTP tool instead. [float] [[console-api]] @@ -137,4 +137,4 @@ shortcuts, click *Help*. If you don’t want to use *Console*, you can disable it by setting `console.ui.enabled` to `false` in your `kibana.yml` configuration file. Changing this setting causes the server to regenerate assets on the next startup, -which might cause a delay before pages start being served. \ No newline at end of file +which might cause a delay before pages start being served. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index a91776fde65ba..0d2d69123b5f3 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -376,6 +376,10 @@ The plugin exposes the static DefaultEditorController class to consume. |The Kibana actions plugin provides a framework to create executable actions. You can: +|{kib-repo}blob/{branch}/x-pack/plugins/aiops/README.md[aiops] +|The plugin provides APIs and components for AIOps features, including the “Explain log rate spikes” UI, maintained by the ML team. + + |{kib-repo}blob/{branch}/x-pack/plugins/alerting/README.md[alerting] |The Kibana Alerting plugin provides a common place to set up rules. You can: diff --git a/package.json b/package.json index 01bb0cf56bd7b..74e4b3e211504 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.2", "@elastic/ems-client": "8.3.0", - "@elastic/eui": "55.0.1", + "@elastic/eui": "55.1.2", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", @@ -554,7 +554,6 @@ "@types/cmd-shim": "^2.0.0", "@types/color": "^3.0.3", "@types/compression-webpack-plugin": "^2.0.2", - "@types/cypress-cucumber-preprocessor": "^1.14.1", "@types/cytoscape": "^3.14.0", "@types/d3": "^3.5.43", "@types/d3-array": "^1.2.7", @@ -810,15 +809,14 @@ "cpy": "^8.1.1", "css-loader": "^3.4.2", "cssnano": "^4.1.11", - "cypress": "^9.2.1", + "cypress": "^9.6.1", "cypress-axe": "^0.14.0", - "cypress-cucumber-preprocessor": "^2.5.2", "cypress-file-upload": "^5.0.8", - "cypress-multi-reporters": "^1.5.0", + "cypress-multi-reporters": "^1.6.0", "cypress-pipe": "^2.0.0", - "cypress-react-selector": "^2.3.13", - "cypress-real-events": "^1.6.0", - "cypress-recurse": "^1.13.1", + "cypress-react-selector": "^2.3.17", + "cypress-real-events": "^1.7.0", + "cypress-recurse": "^1.20.0", "debug": "^2.6.9", "delete-empty": "^2.0.0", "dependency-check": "^4.1.0", diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 818825096ffc1..7b3bc99f916b7 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -112,6 +112,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { synonyms: `${APP_SEARCH_DOCS}synonyms-guide.html`, webCrawler: `${APP_SEARCH_DOCS}web-crawler.html`, webCrawlerEventLogs: `${APP_SEARCH_DOCS}view-web-crawler-events-logs.html`, + webCrawlerReference: `${APP_SEARCH_DOCS}web-crawler-reference.html`, }, enterpriseSearch: { configuration: `${ENTERPRISE_SEARCH_DOCS}configuration.html`, @@ -526,6 +527,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { azureRepo: `${ELASTICSEARCH_DOCS}repository-azure.html`, gcsRepo: `${ELASTICSEARCH_DOCS}repository-gcs.html`, hdfsRepo: `${PLUGIN_DOCS}repository-hdfs.html`, + ingestAttachment: `${PLUGIN_DOCS}ingest-attachment.html`, s3Repo: `${ELASTICSEARCH_DOCS}repository-s3.html`, snapshotRestoreRepos: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html`, mapperSize: `${PLUGIN_DOCS}mapper-size-usage.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 2e14fccaccd29..fdbff542456ea 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -98,6 +98,7 @@ export interface DocLinks { readonly synonyms: string; readonly webCrawler: string; readonly webCrawlerEventLogs: string; + readonly webCrawlerReference: string; }; readonly enterpriseSearch: { readonly configuration: string; @@ -337,6 +338,7 @@ export interface DocLinks { azureRepo: string; gcsRepo: string; hdfsRepo: string; + ingestAttachment: string; s3Repo: string; snapshotRestoreRepos: string; mapperSize: string; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 7fbe272e01bb0..c9460f7bab4ea 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -1,6 +1,7 @@ pageLoadAssetSize: advancedSettings: 27596 actions: 20000 + aiops: 10000 alerting: 106936 apm: 64385 canvas: 1066647 diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap index 069192708e47b..dfdf85d0d3563 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap @@ -2,266 +2,322 @@ exports[`KibanaPageTemplateSolutionNav accepts EuiSideNavProps 1`] = ` - - - Solution - - + initialIsOpen={false} + isCollapsible={true} + paddingSize="m" + title={ + +

+ + + +

+
} - isOpenOnMobile={false} - items={ - Array [ - Object { - "id": "1", - "items": Array [ - Object { - "id": "1.1", - "items": undefined, - "name": "Ingest Node Pipelines", - "tabIndex": undefined, - }, - Object { - "id": "1.2", - "items": undefined, - "name": "Logstash Pipelines", - "tabIndex": undefined, - }, - Object { - "id": "1.3", - "items": undefined, - "name": "Beats Central Management", - "tabIndex": undefined, - }, - ], - "name": "Ingest", - "tabIndex": undefined, - }, - Object { - "id": "2", - "items": Array [ - Object { - "id": "2.1", - "items": undefined, - "name": "Index Management", - "tabIndex": undefined, - }, - Object { - "id": "2.2", - "items": undefined, - "name": "Index Lifecycle Policies", - "tabIndex": undefined, - }, - Object { - "id": "2.3", - "items": undefined, - "name": "Snapshot and Restore", - "tabIndex": undefined, - }, - ], - "name": "Data", - "tabIndex": undefined, - }, - ] - } - mobileTitle={ - - - + titleElement="span" + > + + +
+`; + +exports[`KibanaPageTemplateSolutionNav heading accepts more headingProps 1`] = ` + + +

+ + + +

+ } - toggleOpenOnMobile={[Function]} + titleElement="span" />
`; exports[`KibanaPageTemplateSolutionNav renders 1`] = ` - - - Solution - - + initialIsOpen={false} + isCollapsible={true} + paddingSize="m" + title={ + +

+ + + +

+
} - isOpenOnMobile={false} - items={ - Array [ - Object { - "id": "1", - "items": Array [ - Object { - "id": "1.1", - "items": undefined, - "name": "Ingest Node Pipelines", - "tabIndex": undefined, - }, - Object { - "id": "1.2", - "items": undefined, - "name": "Logstash Pipelines", - "tabIndex": undefined, - }, - Object { - "id": "1.3", - "items": undefined, - "name": "Beats Central Management", - "tabIndex": undefined, - }, - ], - "name": "Ingest", - "tabIndex": undefined, - }, - Object { - "id": "2", - "items": Array [ - Object { - "id": "2.1", - "items": undefined, - "name": "Index Management", - "tabIndex": undefined, - }, - Object { - "id": "2.2", - "items": undefined, - "name": "Index Lifecycle Policies", - "tabIndex": undefined, - }, - Object { - "id": "2.3", - "items": undefined, - "name": "Snapshot and Restore", - "tabIndex": undefined, - }, - ], - "name": "Data", - "tabIndex": undefined, - }, - ] - } - mobileTitle={ - - - - } - toggleOpenOnMobile={[Function]} - /> + titleElement="span" + > + +
`; exports[`KibanaPageTemplateSolutionNav renders with icon 1`] = ` - - - - Solution - - + initialIsOpen={false} + isCollapsible={true} + paddingSize="m" + title={ + +

+ + + + +

+
} - isOpenOnMobile={false} - items={ - Array [ - Object { - "id": "1", - "items": Array [ - Object { - "id": "1.1", - "items": undefined, - "name": "Ingest Node Pipelines", - "tabIndex": undefined, - }, - Object { - "id": "1.2", - "items": undefined, - "name": "Logstash Pipelines", - "tabIndex": undefined, - }, - Object { - "id": "1.3", - "items": undefined, - "name": "Beats Central Management", - "tabIndex": undefined, - }, - ], - "name": "Ingest", - "tabIndex": undefined, - }, - Object { - "id": "2", - "items": Array [ - Object { - "id": "2.1", - "items": undefined, - "name": "Index Management", - "tabIndex": undefined, - }, - Object { - "id": "2.2", - "items": undefined, - "name": "Index Lifecycle Policies", - "tabIndex": undefined, - }, - Object { - "id": "2.3", - "items": undefined, - "name": "Snapshot and Restore", - "tabIndex": undefined, - }, - ], - "name": "Data", - "tabIndex": undefined, - }, - ] - } - mobileTitle={ - - - - - } - toggleOpenOnMobile={[Function]} - /> + titleElement="span" + > + +
`; diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss index d0070cef729b7..91b96641047e8 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss @@ -15,7 +15,7 @@ $euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7); padding: $euiSizeL; } - .kbnPageTemplateSolutionNavAvatar { + .kbnPageTemplateSolutionNav__avatar { margin-right: $euiSize; } } diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.stories.tsx b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.stories.tsx index 5ff1e2c07d9d8..28550a7789a9f 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.stories.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.stories.tsx @@ -69,4 +69,12 @@ PureComponent.argTypes = { options: ['logoKibana', 'logoObservability', 'logoSecurity'], defaultValue: 'logoKibana', }, + children: { + control: 'text', + defaultValue: '', + }, +}; + +PureComponent.parameters = { + layout: 'fullscreen', }; diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.test.tsx b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.test.tsx index 2792ae518e5a2..9e2eac4cf20d6 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.test.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.test.tsx @@ -10,14 +10,15 @@ import React from 'react'; import { shallow } from 'enzyme'; import { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav'; -jest.mock('@elastic/eui', () => ({ - useIsWithinBreakpoints: (args: string[]) => { - return args[0] === 'xs'; - }, - EuiSideNav: function Component() { - // no-op - }, -})); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + useIsWithinBreakpoints: (args: string[]) => { + return args[0] === 'xs'; + }, + }; +}); const items: KibanaPageTemplateSolutionNavProps['items'] = [ { @@ -59,6 +60,19 @@ const items: KibanaPageTemplateSolutionNavProps['items'] = [ ]; describe('KibanaPageTemplateSolutionNav', () => { + describe('heading', () => { + test('accepts more headingProps', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + test('renders', () => { const component = shallow(); expect(component).toMatchSnapshot(); @@ -71,6 +85,15 @@ describe('KibanaPageTemplateSolutionNav', () => { expect(component).toMatchSnapshot(); }); + test('renders with children', () => { + const component = shallow( + + + + ); + expect(component.find('#dummy_component').length > 0).toBeTruthy(); + }); + test('accepts EuiSideNavProps', () => { const component = shallow( diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.tsx b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.tsx index 4993d910e08be..191e56db530a9 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.tsx @@ -7,23 +7,31 @@ */ import './solution_nav.scss'; -import React, { FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useState, useMemo } from 'react'; import classNames from 'classnames'; import { EuiAvatarProps, + EuiCollapsibleNavGroup, EuiFlyout, + EuiFlyoutProps, EuiSideNav, EuiSideNavItemType, EuiSideNavProps, + EuiSpacer, + EuiTitle, + htmlIdGenerator, useIsWithinBreakpoints, } from '@elastic/eui'; - import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution'; import { KibanaPageTemplateSolutionNavCollapseButton } from './solution_nav_collapse_button'; -export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & { +export type KibanaPageTemplateSolutionNavProps = Omit< + EuiSideNavProps<{}>, + 'children' | 'items' | 'heading' +> & { /** * Name of the solution, i.e. "Observability" */ @@ -32,6 +40,19 @@ export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & { * Solution logo, i.e. "logoObservability" */ icon?: EuiAvatarProps['iconType']; + /** + * An array of #EuiSideNavItem objects. Lists navigation menu items. + */ + items?: EuiSideNavProps<{}>['items']; + /** + * Renders the children instead of default EuiSideNav + */ + children?: React.ReactNode; + /** + * The position of the close button when the navigation flyout is open. + * Note that side navigation turns into a flyout only when the screen has medium size. + */ + closeFlyoutButtonPosition?: EuiFlyoutProps['closeButtonPosition']; /** * Control the collapsed state */ @@ -50,13 +71,26 @@ const setTabIndex = (items: Array>, isHidden: boolean) => }); }; +const generateId = htmlIdGenerator('KibanaPageTemplateSolutionNav'); + /** * A wrapper around EuiSideNav but also creates the appropriate title with optional solution logo */ export const KibanaPageTemplateSolutionNav: FunctionComponent< KibanaPageTemplateSolutionNavProps -> = ({ name, icon, items, isOpenOnDesktop = false, onCollapse, ...rest }) => { - const isSmallerBreakpoint = useIsWithinBreakpoints(['xs', 's']); +> = ({ + children, + headingProps, + icon, + isOpenOnDesktop = false, + items, + mobileBreakpoints = ['xs', 's'], + closeFlyoutButtonPosition = 'outside', + name, + onCollapse, + ...rest +}) => { + const isSmallerBreakpoint = useIsWithinBreakpoints(mobileBreakpoints); const isMediumBreakpoint = useIsWithinBreakpoints(['m']); const isLargerBreakpoint = useIsWithinBreakpoints(['l', 'xl']); @@ -67,68 +101,81 @@ export const KibanaPageTemplateSolutionNav: FunctionComponent< }; const isHidden = isLargerBreakpoint && !isOpenOnDesktop; + const isCustomSideNav = !!children; - /** - * Create the avatar - */ - const solutionAvatar = icon ? ( - - ) : null; + const sideNavClasses = classNames('kbnPageTemplateSolutionNav', { + 'kbnPageTemplateSolutionNav--hidden': isHidden, + }); /** - * Create the titles + * Create the avatar and titles */ + const headingID = headingProps?.id || generateId('heading'); + const HeadingElement = headingProps?.element || 'h2'; const titleText = ( - <> - {solutionAvatar} - {name} - - ); - const mobileTitleText = ( - + + + {icon && ( + + )} + + + + + ); /** - * Create the side nav component + * Create the side nav content */ - - const sideNav = () => { + const sideNavContent = useMemo(() => { + if (isCustomSideNav) { + return children; + } if (!items) { return null; } - const sideNavClasses = classNames('kbnPageTemplateSolutionNav', { - 'kbnPageTemplateSolutionNav--hidden': isHidden, - }); return ( - {solutionAvatar} - {mobileTitleText} - - } - toggleOpenOnMobile={toggleOpenOnMobile} - isOpenOnMobile={isSideNavOpenOnMobile} items={setTabIndex(items, isHidden)} + mobileBreakpoints={[]} // prevent EuiSideNav to apply mobile version, already implemented here {...rest} /> ); - }; + }, [children, headingID, isCustomSideNav, isHidden, items, rest]); return ( <> - {isSmallerBreakpoint && sideNav()} + {isSmallerBreakpoint && ( + + {sideNavContent} + + )} {isMediumBreakpoint && ( <> {isSideNavOpenOnMobile && ( @@ -138,10 +185,14 @@ export const KibanaPageTemplateSolutionNav: FunctionComponent< onClose={() => setIsSideNavOpenOnMobile(false)} side="left" size={FLYOUT_SIZE} - closeButtonPosition="outside" + closeButtonPosition={closeFlyoutButtonPosition} className="kbnPageTemplateSolutionNav__flyout" > - {sideNav()} +
+ {titleText} + + {sideNavContent} +
)} - {sideNav()} +
+ {titleText} + + {sideNavContent} +
} - closeNav={[Function]} - customNavLink$={ - BehaviorSubject { - "_value": Object { - "baseUrl": "/", - "category": undefined, - "data-test-subj": "Custom link", - "href": "Custom link", - "id": "Custom link", - "isActive": true, - "title": "Custom link", - "url": "/", - }, - "closed": false, - "currentObservers": null, - "hasError": false, - "isStopped": false, - "observers": Array [ - SafeSubscriber { - "_finalizers": Array [ - Subscription { - "_finalizers": null, - "_parentage": [Circular], - "closed": false, - "initialTeardown": [Function], - }, - ], - "_parentage": null, - "closed": false, - "destination": ConsumerObserver { - "partialObserver": Object { - "complete": undefined, - "error": undefined, - "next": [Function], - }, - }, - "initialTeardown": undefined, - "isStopped": false, - }, - ], - "thrownError": null, - } - } - homeHref="/" - id="collapsibe-nav" - isLocked={false} - isNavOpen={true} - navLinks$={ - BehaviorSubject { - "_value": Array [ - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoKibana", - "id": "kibana", - "label": "Analytics", - "order": 1000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - "url": "/", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoSecurity", - "id": "securitySolution", - "label": "Security", - "order": 4000, - }, - "data-test-subj": "siem", - "href": "siem", - "id": "siem", - "isActive": true, - "title": "siem", - "url": "/", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoObservability", - "id": "observability", - "label": "Observability", - "order": 3000, - }, - "data-test-subj": "metrics", - "href": "metrics", - "id": "metrics", - "isActive": true, - "title": "metrics", - "url": "/", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "managementApp", - "id": "management", - "label": "Management", - "order": 5000, - }, - "data-test-subj": "monitoring", - "href": "monitoring", - "id": "monitoring", - "isActive": true, - "title": "monitoring", - "url": "/", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoKibana", - "id": "kibana", - "label": "Analytics", - "order": 1000, - }, - "data-test-subj": "visualize", - "href": "visualize", - "id": "visualize", - "isActive": true, - "title": "visualize", - "url": "/", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoKibana", - "id": "kibana", - "label": "Analytics", - "order": 1000, - }, - "data-test-subj": "dashboard", - "href": "dashboard", - "id": "dashboard", - "isActive": true, - "title": "dashboard", - "url": "/", - }, - Object { - "baseUrl": "/", - "category": undefined, - "data-test-subj": "canvas", - "href": "canvas", - "id": "canvas", - "isActive": true, - "title": "canvas", - "url": "/", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoObservability", - "id": "observability", - "label": "Observability", - "order": 3000, - }, - "data-test-subj": "logs", - "href": "logs", - "id": "logs", - "isActive": true, - "title": "logs", - "url": "/", - }, - ], - "closed": false, - "currentObservers": null, - "hasError": false, - "isStopped": false, - "observers": Array [ - SafeSubscriber { - "_finalizers": Array [ - Subscription { - "_finalizers": null, - "_parentage": [Circular], - "closed": false, - "initialTeardown": [Function], - }, - ], - "_parentage": null, - "closed": false, - "destination": ConsumerObserver { - "partialObserver": Object { - "complete": undefined, - "error": undefined, - "next": [Function], - }, - }, - "initialTeardown": undefined, - "isStopped": false, - }, - ], - "thrownError": null, - } - } - navigateToApp={[Function]} - navigateToUrl={[Function]} - onIsLockedUpdate={[Function]} - recentlyAccessed$={ - BehaviorSubject { - "_value": Array [ - Object { - "id": "recent 1", - "label": "recent 1", - "link": "recent 1", - }, - Object { - "id": "recent 2", - "label": "recent 2", - "link": "recent 2", - }, - ], - "closed": false, - "currentObservers": null, - "hasError": false, - "isStopped": false, - "observers": Array [ - SafeSubscriber { - "_finalizers": Array [ - Subscription { - "_finalizers": null, - "_parentage": [Circular], - "closed": false, - "initialTeardown": [Function], - }, - ], - "_parentage": null, - "closed": false, - "destination": ConsumerObserver { - "partialObserver": Object { - "complete": undefined, - "error": undefined, - "next": [Function], - }, - }, - "initialTeardown": undefined, - "isStopped": false, - }, - ], - "thrownError": null, - } - } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } - url="/" -> - } +Array [ + + + + + + + +
+
+
+
+ - +
+
- - -
-
- -
-
-
- - - -
-
-
-
+ Analytics + +
-
- - - -
-
- -
+ + +
+
+
+
- - - - - - -

- Analytics -

-
-
- - } - buttonElement="button" - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-kibana" - element="div" - id="generated-id" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" +
-
-
- - - - -
-
- -
-
-
- - - -
-
-
-
-
-
- - - - + +
  • - - - - - -

    - Observability -

    -
    -
    - - } - buttonElement="button" - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-observability" - element="div" - id="generated-id" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" - > -
    -
    + +
  • +
  • - - - - -
  • -
    + + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    - - - - - - -

    - Security -

    -
    -
    - - } - buttonElement="button" - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-securitySolution" - element="div" - id="generated-id" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" +
    -
    -
    - - + +
  • + - - -
  • -
    + + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    - - - - - - -

    - Management -

    -
    -
    - - } - buttonElement="button" - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-management" - element="div" - id="generated-id" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" +
    -
    -
    - - - - -
    -
    + + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    -
    - - - -
    + monitoring + + + +
    - +
    - - - - - +
    +
    +
    +
    + +
    +
    +
    + , +] `; exports[`CollapsibleNav renders the default nav 1`] = ` diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 0102343ca6eb7..787fdc031f1a5 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -113,7 +113,7 @@ describe('CollapsibleNav', () => { customNavLink$={new BehaviorSubject(customNavLink)} /> ); - expect(component).toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); it('remembers collapsible section state', () => { diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index a3e3ca7a7c207..777e7876c1476 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -141,6 +141,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiImage.openImage": [Function], "euiLink.external.ariaLabel": "External link", "euiLink.newTarget.screenReaderOnlyText": "(opens in a new tab or window)", + "euiLoadingChart.ariaLabel": "Loading", "euiMark.highlightEnd": "highlight end", "euiMark.highlightStart": "highlight start", "euiMarkdownEditorFooter.closeButton": "Close", diff --git a/src/core/public/i18n/i18n_eui_mapping.tsx b/src/core/public/i18n/i18n_eui_mapping.tsx index 5344fddc4fe2e..bf14153ef0337 100644 --- a/src/core/public/i18n/i18n_eui_mapping.tsx +++ b/src/core/public/i18n/i18n_eui_mapping.tsx @@ -618,6 +618,9 @@ export const getEuiContextMapping = (): EuiTokensObject => { defaultMessage: '(opens in a new tab or window)', } ), + 'euiLoadingChart.ariaLabel': i18n.translate('core.euiLoadingChart.ariaLabel', { + defaultMessage: 'Loading', + }), 'euiMark.highlightStart': i18n.translate('core.euiMark.highlightStart', { defaultMessage: 'highlight start', }), diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index bd5fd75e30998..0ccab6fcf1b24 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -77,6 +77,6 @@ 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 '@elastic/ems-client@8.3.0': ['Elastic License 2.0'], - '@elastic/eui@55.0.1': ['SSPL-1.0 OR Elastic License 2.0'], + '@elastic/eui@55.1.2': ['SSPL-1.0 OR Elastic License 2.0'], '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/run_precommit_hook.js b/src/dev/run_precommit_hook.js index bc50aeddc619e..a86bb5c7dabcc 100644 --- a/src/dev/run_precommit_hook.js +++ b/src/dev/run_precommit_hook.js @@ -49,7 +49,7 @@ run( fix: flags.fix, }); - if (flags.fix) { + if (flags.fix && flags.stage) { const simpleGit = new SimpleGit(REPO_ROOT); await simpleGit.add(filesToLint); } @@ -68,16 +68,18 @@ run( Run checks on files that are staged for commit by default `, flags: { - boolean: ['fix'], + boolean: ['fix', 'stage'], string: ['max-files', 'ref'], default: { fix: false, + stage: true, }, help: ` - --fix Execute eslint in --fix mode - --max-files Max files number to check against. If exceeded the script will skip the execution - --ref Run checks against any git ref files (example HEAD or ) instead of running against staged ones - `, + --fix Execute eslint in --fix mode + --max-files Max files number to check against. If exceeded the script will skip the execution + --ref Run checks against any git ref files (example HEAD or ) instead of running against staged ones + --no-stage By default when using --fix the changes are staged, use --no-stage to disable that behavior + `, }, } ); diff --git a/src/plugins/controls/common/control_types/options_list/types.ts b/src/plugins/controls/common/control_types/options_list/types.ts index 0f889bed7bacb..7dfdfab742d1a 100644 --- a/src/plugins/controls/common/control_types/options_list/types.ts +++ b/src/plugins/controls/common/control_types/options_list/types.ts @@ -6,27 +6,60 @@ * Side Public License, v 1. */ -import { BoolQuery } from '@kbn/es-query'; -import { FieldSpec } from '@kbn/data-views-plugin/common'; +import { TimeRange } from '@kbn/data-plugin/common'; +import { Filter, Query, BoolQuery } from '@kbn/es-query'; +import { FieldSpec, DataView, DataViewField } from '@kbn/data-views-plugin/common'; + import { DataControlInput } from '../../types'; export const OPTIONS_LIST_CONTROL = 'optionsListControl'; export interface OptionsListEmbeddableInput extends DataControlInput { selectedOptions?: string[]; + runPastTimeout?: boolean; + textFieldName?: string; singleSelect?: boolean; loading?: boolean; } +export type OptionsListField = DataViewField & { + textFieldName?: string; + parentFieldName?: string; + childFieldName?: string; +}; + +/** + * The Options list response is returned from the serverside Options List route. + */ export interface OptionsListResponse { suggestions: string[]; totalCardinality: number; invalidSelections?: string[]; } +/** + * The Options list request type taken in by the public Options List service. + */ +export type OptionsListRequest = Omit< + OptionsListRequestBody, + 'filters' | 'fieldName' | 'fieldSpec' | 'textFieldName' +> & { + timeRange?: TimeRange; + field: OptionsListField; + runPastTimeout?: boolean; + dataView: DataView; + filters?: Filter[]; + query?: Query; +}; + +/** + * The Options list request body is sent to the serverside Options List route and is used to create the ES query. + */ export interface OptionsListRequestBody { filters?: Array<{ bool: BoolQuery }>; selectedOptions?: string[]; + runPastTimeout?: boolean; + textFieldName?: string; searchString?: string; fieldSpec?: FieldSpec; fieldName: string; diff --git a/src/plugins/controls/public/__stories__/controls.stories.tsx b/src/plugins/controls/public/__stories__/controls.stories.tsx index 481016af72a36..e8133e7dae503 100644 --- a/src/plugins/controls/public/__stories__/controls.stories.tsx +++ b/src/plugins/controls/public/__stories__/controls.stories.tsx @@ -31,12 +31,11 @@ import { decorators } from './decorators'; import { ControlsPanels } from '../control_group/types'; import { ControlGroupContainer } from '../control_group'; import { pluginServices, registry } from '../services/storybook'; -import { replaceValueSuggestionMethod } from '../services/storybook/unified_search'; import { injectStorybookDataView } from '../services/storybook/data_views'; -import { populateStorybookControlFactories } from './storybook_control_factories'; -import { OptionsListRequest } from '../services/options_list'; -import { OptionsListResponse } from '../control_types/options_list/types'; import { replaceOptionsListMethod } from '../services/storybook/options_list'; +import { populateStorybookControlFactories } from './storybook_control_factories'; +import { replaceValueSuggestionMethod } from '../services/storybook/unified_search'; +import { OptionsListResponse, OptionsListRequest } from '../control_types/options_list/types'; export default { title: 'Controls', diff --git a/src/plugins/controls/public/control_group/component/control_frame_component.tsx b/src/plugins/controls/public/control_group/component/control_frame_component.tsx index 36879bea110d8..dabe351376b7f 100644 --- a/src/plugins/controls/public/control_group/component/control_frame_component.tsx +++ b/src/plugins/controls/public/control_group/component/control_frame_component.tsx @@ -28,9 +28,15 @@ export interface ControlFrameProps { customPrepend?: JSX.Element; enableActions?: boolean; embeddableId: string; + embeddableType: string; } -export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: ControlFrameProps) => { +export const ControlFrame = ({ + customPrepend, + enableActions, + embeddableId, + embeddableType, +}: ControlFrameProps) => { const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []); const { useEmbeddableSelector, @@ -42,7 +48,7 @@ export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: Con const { overlays } = pluginServices.getHooks(); const { openConfirm } = overlays.useService(); - const embeddable = useChildEmbeddable({ untilEmbeddableLoaded, embeddableId }); + const embeddable = useChildEmbeddable({ untilEmbeddableLoaded, embeddableId, embeddableType }); const [title, setTitle] = useState(); diff --git a/src/plugins/controls/public/control_group/component/control_group_component.tsx b/src/plugins/controls/public/control_group/component/control_group_component.tsx index 3abee52002db1..72dc49b2f9fbb 100644 --- a/src/plugins/controls/public/control_group/component/control_group_component.tsx +++ b/src/plugins/controls/public/control_group/component/control_group_component.tsx @@ -144,6 +144,7 @@ export const ControlGroup = () => { isEditable={isEditable} dragInfo={{ index, draggingIndex }} embeddableId={controlId} + embeddableType={panels[controlId].type} key={controlId} /> ) diff --git a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx index 2741752b4df88..bdf1851a0daa1 100644 --- a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx +++ b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx @@ -60,44 +60,50 @@ export const SortableControl = (frameProps: SortableControlProps) => { const SortableControlInner = forwardRef< HTMLButtonElement, SortableControlProps & { style: HTMLAttributes['style'] } ->(({ embeddableId, dragInfo, style, isEditable, ...dragHandleProps }, dragHandleRef) => { - const { isOver, isDragging, draggingIndex, index } = dragInfo; - const { useEmbeddableSelector } = useReduxContainerContext(); - const { panels } = useEmbeddableSelector((state) => state); +>( + ( + { embeddableId, embeddableType, dragInfo, style, isEditable, ...dragHandleProps }, + dragHandleRef + ) => { + const { isOver, isDragging, draggingIndex, index } = dragInfo; + const { useEmbeddableSelector } = useReduxContainerContext(); + const { panels } = useEmbeddableSelector((state) => state); - const width = panels[embeddableId].width; + const width = panels[embeddableId].width; - const dragHandle = ( - - ); + const dragHandle = ( + + ); - return ( - (draggingIndex ?? -1), - })} - style={style} - > - - - ); -}); + return ( + (draggingIndex ?? -1), + })} + style={style} + > + + + ); + } +); /** * A simplified clone version of the control which is dragged. This version only shows diff --git a/src/plugins/controls/public/control_group/editor/control_editor.tsx b/src/plugins/controls/public/control_group/editor/control_editor.tsx index 3cd5b92e503c1..eb7eff4abb42a 100644 --- a/src/plugins/controls/public/control_group/editor/control_editor.tsx +++ b/src/plugins/controls/public/control_group/editor/control_editor.tsx @@ -41,6 +41,7 @@ import { ControlEmbeddable, ControlInput, ControlWidth, + DataControlInput, IEditableControlFactory, } from '../../types'; import { CONTROL_WIDTH_OPTIONS } from './editor_constants'; @@ -85,6 +86,11 @@ export const ControlEditor = ({ const [currentTitle, setCurrentTitle] = useState(title); const [currentWidth, setCurrentWidth] = useState(width); const [controlEditorValid, setControlEditorValid] = useState(false); + const [selectedField, setSelectedField] = useState( + embeddable + ? (embeddable.getInput() as DataControlInput).fieldName // CLEAN THIS ONCE OTHER PR GETS IN + : undefined + ); const getControlTypeEditor = (type: string) => { const factory = getControlFactory(type); @@ -96,6 +102,8 @@ export const ControlEditor = ({ onChange={onTypeEditorChange} setValidState={setControlEditorValid} initialInput={embeddable?.getInput()} + selectedField={selectedField} + setSelectedField={setSelectedField} setDefaultTitle={(newDefaultTitle) => { if (!currentTitle || currentTitle === defaultTitle) { setCurrentTitle(newDefaultTitle); @@ -107,8 +115,8 @@ export const ControlEditor = ({ ) : null; }; - const getTypeButtons = (controlTypes: string[]) => { - return controlTypes.map((type) => { + const getTypeButtons = () => { + return getControlTypes().map((type) => { const factory = getControlFactory(type); const icon = (factory as EmbeddableFactoryDefinition).getIconType?.(); const tooltip = (factory as EmbeddableFactoryDefinition).getDescription?.(); @@ -120,6 +128,12 @@ export const ControlEditor = ({ isSelected={selectedType === type} onClick={() => { setSelectedType(type); + if (!isCreate) + setSelectedField( + embeddable && type === embeddable.type + ? (embeddable.getInput() as DataControlInput).fieldName + : undefined + ); }} > @@ -150,9 +164,7 @@ export const ControlEditor = ({ - - {isCreate ? getTypeButtons(getControlTypes()) : getTypeButtons([selectedType])} - + {getTypeButtons()} {selectedType && ( <> diff --git a/src/plugins/controls/public/control_group/editor/edit_control.tsx b/src/plugins/controls/public/control_group/editor/edit_control.tsx index 11a2e705a13f3..6866148ac7e9d 100644 --- a/src/plugins/controls/public/control_group/editor/edit_control.tsx +++ b/src/plugins/controls/public/control_group/editor/edit_control.tsx @@ -22,6 +22,11 @@ import { IEditableControlFactory, ControlInput } from '../../types'; import { controlGroupReducers } from '../state/control_group_reducers'; import { ControlGroupContainer, setFlyoutRef } from '../embeddable/control_group_container'; +interface EditControlResult { + type: string; + controlInput: Omit; +} + export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => { // Controls Services Context const { overlays, controls } = pluginServices.getHooks(); @@ -34,7 +39,7 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => typeof controlGroupReducers >(); const { - containerActions: { untilEmbeddableLoaded, removeEmbeddable, updateInputForChild }, + containerActions: { untilEmbeddableLoaded, removeEmbeddable, replaceEmbeddable }, actions: { setControlWidth }, useEmbeddableSelector, useEmbeddableDispatch, @@ -52,88 +57,107 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => const editControl = async () => { const panel = panels[embeddableId]; - const factory = getControlFactory(panel.type); + let factory = getControlFactory(panel.type); + if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type); + const embeddable = await untilEmbeddableLoaded(embeddableId); const controlGroup = embeddable.getRoot() as ControlGroupContainer; - let inputToReturn: Partial = {}; + const initialInputPromise = new Promise((resolve, reject) => { + let inputToReturn: Partial = {}; - if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type); + let removed = false; + const onCancel = (ref: OverlayRef) => { + if ( + removed || + (isEqual(latestPanelState.current.explicitInput, { + ...panel.explicitInput, + ...inputToReturn, + }) && + isEqual(latestPanelState.current.width, panel.width)) + ) { + reject(); + ref.close(); + return; + } + openConfirm(ControlGroupStrings.management.discardChanges.getSubtitle(), { + confirmButtonText: ControlGroupStrings.management.discardChanges.getConfirm(), + cancelButtonText: ControlGroupStrings.management.discardChanges.getCancel(), + title: ControlGroupStrings.management.discardChanges.getTitle(), + buttonColor: 'danger', + }).then((confirmed) => { + if (confirmed) { + dispatch(setControlWidth({ width: panel.width, embeddableId })); + reject(); + ref.close(); + } + }); + }; - let removed = false; - const onCancel = (ref: OverlayRef) => { - if ( - removed || - (isEqual(latestPanelState.current.explicitInput, { - ...panel.explicitInput, - ...inputToReturn, - }) && - isEqual(latestPanelState.current.width, panel.width)) - ) { + const onSave = (type: string, ref: OverlayRef) => { + // if the control now has a new type, need to replace the old factory with + // one of the correct new type + if (latestPanelState.current.type !== type) { + factory = getControlFactory(type); + if (!factory) throw new EmbeddableFactoryNotFoundError(type); + } + const editableFactory = factory as IEditableControlFactory; + if (editableFactory.presaveTransformFunction) { + inputToReturn = editableFactory.presaveTransformFunction(inputToReturn, embeddable); + } + resolve({ type, controlInput: inputToReturn }); ref.close(); - return; - } - openConfirm(ControlGroupStrings.management.discardChanges.getSubtitle(), { - confirmButtonText: ControlGroupStrings.management.discardChanges.getConfirm(), - cancelButtonText: ControlGroupStrings.management.discardChanges.getCancel(), - title: ControlGroupStrings.management.discardChanges.getTitle(), - buttonColor: 'danger', - }).then((confirmed) => { - if (confirmed) { - dispatch(setControlWidth({ width: panel.width, embeddableId })); - ref.close(); + }; + + const flyoutInstance = openFlyout( + forwardAllContext( + onCancel(flyoutInstance)} + updateTitle={(newTitle) => (inputToReturn.title = newTitle)} + setLastUsedDataViewId={(lastUsed) => controlGroup.setLastUsedDataViewId(lastUsed)} + updateWidth={(newWidth) => dispatch(setControlWidth({ width: newWidth, embeddableId }))} + onTypeEditorChange={(partialInput) => { + inputToReturn = { ...inputToReturn, ...partialInput }; + }} + onSave={(type) => onSave(type, flyoutInstance)} + removeControl={() => { + openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), { + confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(), + cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(), + title: ControlGroupStrings.management.deleteControls.getDeleteTitle(), + buttonColor: 'danger', + }).then((confirmed) => { + if (confirmed) { + removeEmbeddable(embeddableId); + removed = true; + flyoutInstance.close(); + } + }); + }} + />, + reduxContainerContext + ), + { + outsideClickCloses: false, + onClose: (flyout) => { + setFlyoutRef(undefined); + onCancel(flyout); + }, } - }); - }; + ); + setFlyoutRef(flyoutInstance); + }); - const flyoutInstance = openFlyout( - forwardAllContext( - onCancel(flyoutInstance)} - updateTitle={(newTitle) => (inputToReturn.title = newTitle)} - setLastUsedDataViewId={(lastUsed) => controlGroup.setLastUsedDataViewId(lastUsed)} - updateWidth={(newWidth) => dispatch(setControlWidth({ width: newWidth, embeddableId }))} - onTypeEditorChange={(partialInput) => - (inputToReturn = { ...inputToReturn, ...partialInput }) - } - onSave={() => { - const editableFactory = factory as IEditableControlFactory; - if (editableFactory.presaveTransformFunction) { - inputToReturn = editableFactory.presaveTransformFunction(inputToReturn, embeddable); - } - updateInputForChild(embeddableId, inputToReturn); - flyoutInstance.close(); - }} - removeControl={() => { - openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), { - confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(), - cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(), - title: ControlGroupStrings.management.deleteControls.getDeleteTitle(), - buttonColor: 'danger', - }).then((confirmed) => { - if (confirmed) { - removeEmbeddable(embeddableId); - removed = true; - flyoutInstance.close(); - } - }); - }} - />, - reduxContainerContext - ), - { - outsideClickCloses: false, - onClose: (flyout) => { - setFlyoutRef(undefined); - onCancel(flyout); - }, - } + initialInputPromise.then( + async (promise) => { + await replaceEmbeddable(embeddable.id, promise.controlInput, promise.type); + }, + () => {} // swallow promise rejection because it can be part of normal flow ); - setFlyoutRef(flyoutInstance); }; return ( diff --git a/src/plugins/controls/public/control_types/options_list/options_list_editor.tsx b/src/plugins/controls/public/control_types/options_list/options_list_editor.tsx index d77cf2b2c1a71..b6d5a0877d7ce 100644 --- a/src/plugins/controls/public/control_types/options_list/options_list_editor.tsx +++ b/src/plugins/controls/public/control_types/options_list/options_list_editor.tsx @@ -8,26 +8,26 @@ import useMount from 'react-use/lib/useMount'; import React, { useEffect, useState } from 'react'; -import { EuiFormRow, EuiSwitch } from '@elastic/eui'; -import { DataViewListItem, DataView } from '@kbn/data-views-plugin/common'; import { LazyDataViewPicker, LazyFieldPicker, withSuspense, } from '@kbn/presentation-util-plugin/public'; +import { IFieldSubTypeMulti } from '@kbn/es-query'; +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { DataViewListItem, DataView } from '@kbn/data-views-plugin/common'; + import { pluginServices } from '../../services'; import { ControlEditorProps } from '../../types'; -import { OptionsListEmbeddableInput } from './types'; import { OptionsListStrings } from './options_list_strings'; - +import { OptionsListEmbeddableInput, OptionsListField } from './types'; interface OptionsListEditorState { singleSelect?: boolean; - + runPastTimeout?: boolean; dataViewListItems: DataViewListItem[]; - + fieldsMap?: { [key: string]: OptionsListField }; dataView?: DataView; - fieldName?: string; } const FieldPicker = withSuspense(LazyFieldPicker, null); @@ -40,20 +40,22 @@ export const OptionsListEditor = ({ setDefaultTitle, getRelevantDataViewId, setLastUsedDataViewId, + selectedField, + setSelectedField, }: ControlEditorProps) => { // Controls Services Context const { dataViews } = pluginServices.getHooks(); const { getIdsWithTitle, getDefaultId, get } = dataViews.useService(); const [state, setState] = useState({ - fieldName: initialInput?.fieldName, singleSelect: initialInput?.singleSelect, + runPastTimeout: initialInput?.runPastTimeout, dataViewListItems: [], }); useMount(() => { let mounted = true; - if (state.fieldName) setDefaultTitle(state.fieldName); + if (selectedField) setDefaultTitle(selectedField); (async () => { const dataViewListItems = await getIdsWithTitle(); const initialId = @@ -64,19 +66,60 @@ export const OptionsListEditor = ({ dataView = await get(initialId); } if (!mounted) return; - setState((s) => ({ ...s, dataView, dataViewListItems })); + setState((s) => ({ ...s, dataView, dataViewListItems, fieldsMap: {} })); })(); return () => { mounted = false; }; }); + useEffect(() => { + if (!state.dataView) return; + + // double link the parent-child relationship so that we can filter in fields which are multi-typed to text / keyword + const doubleLinkedFields: OptionsListField[] = state.dataView?.fields.getAll(); + for (const field of doubleLinkedFields) { + const parentFieldName = (field.subType as IFieldSubTypeMulti)?.multi?.parent; + if (parentFieldName) { + (field as OptionsListField).parentFieldName = parentFieldName; + const parentField = state.dataView?.getFieldByName(parentFieldName); + (parentField as OptionsListField).childFieldName = field.name; + } + } + + const newFieldsMap: OptionsListEditorState['fieldsMap'] = {}; + for (const field of doubleLinkedFields) { + if (field.type === 'boolean') { + newFieldsMap[field.name] = field; + } + + // field type is keyword, check if this field is related to a text mapped field and include it. + else if (field.aggregatable && field.type === 'string') { + const childField = + (field.childFieldName && state.dataView?.fields.getByName(field.childFieldName)) || + undefined; + const parentField = + (field.parentFieldName && state.dataView?.fields.getByName(field.parentFieldName)) || + undefined; + + const textFieldName = childField?.esTypes?.includes('text') + ? childField.name + : parentField?.esTypes?.includes('text') + ? parentField.name + : undefined; + + newFieldsMap[field.name] = { ...field, textFieldName } as OptionsListField; + } + } + setState((s) => ({ ...s, fieldsMap: newFieldsMap })); + }, [state.dataView]); + useEffect( - () => setValidState(Boolean(state.fieldName) && Boolean(state.dataView)), - [state.fieldName, setValidState, state.dataView] + () => setValidState(Boolean(selectedField) && Boolean(state.dataView)), + [selectedField, setValidState, state.dataView] ); - const { dataView, fieldName } = state; + const { dataView } = state; return ( <> @@ -88,7 +131,7 @@ export const OptionsListEditor = ({ if (dataViewId === dataView?.id) return; onChange({ dataViewId }); - setState((s) => ({ ...s, fieldName: undefined })); + setSelectedField(undefined); get(dataViewId).then((newDataView) => { setState((s) => ({ ...s, dataView: newDataView })); }); @@ -100,15 +143,17 @@ export const OptionsListEditor = ({ - (field.aggregatable && field.type === 'string') || field.type === 'boolean' - } - selectedFieldName={fieldName} + filterPredicate={(field) => Boolean(state.fieldsMap?.[field.name])} + selectedFieldName={selectedField} dataView={dataView} onSelectField={(field) => { setDefaultTitle(field.displayName ?? field.name); - onChange({ fieldName: field.name }); - setState((s) => ({ ...s, fieldName: field.name })); + const textFieldName = state.fieldsMap?.[field.name].textFieldName; + onChange({ + fieldName: field.name, + textFieldName, + }); + setSelectedField(field.name); }} /> @@ -122,6 +167,16 @@ export const OptionsListEditor = ({ }} /> + + { + onChange({ runPastTimeout: !state.runPastTimeout }); + setState((s) => ({ ...s, runPastTimeout: !s.runPastTimeout })); + }} + /> + ); }; diff --git a/src/plugins/controls/public/control_types/options_list/options_list_embeddable.tsx b/src/plugins/controls/public/control_types/options_list/options_list_embeddable.tsx index b315fd00392ea..edf4cb6ddaff1 100644 --- a/src/plugins/controls/public/control_types/options_list/options_list_embeddable.tsx +++ b/src/plugins/controls/public/control_types/options_list/options_list_embeddable.tsx @@ -20,21 +20,22 @@ import deepEqual from 'fast-deep-equal'; import { merge, Subject, Subscription, BehaviorSubject } from 'rxjs'; import { tap, debounceTime, map, distinctUntilChanged, skip } from 'rxjs/operators'; -import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; -import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public'; import { withSuspense, LazyReduxEmbeddableWrapper, ReduxEmbeddableWrapperPropsWithChildren, } from '@kbn/presentation-util-plugin/public'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public'; + +import { OptionsListEmbeddableInput, OptionsListField, OPTIONS_LIST_CONTROL } from './types'; import { OptionsListComponent, OptionsListComponentState } from './options_list_component'; -import { OptionsListEmbeddableInput, OPTIONS_LIST_CONTROL } from './types'; +import { ControlsOptionsListService } from '../../services/options_list'; import { ControlsDataViewsService } from '../../services/data_views'; import { optionsListReducers } from './options_list_reducers'; import { OptionsListStrings } from './options_list_strings'; import { ControlInput, ControlOutput } from '../..'; import { pluginServices } from '../../services'; -import { ControlsOptionsListService } from '../../services/options_list'; const OptionsListReduxWrapper = withSuspense< ReduxEmbeddableWrapperPropsWithChildren @@ -76,7 +77,7 @@ export class OptionsListEmbeddable extends Embeddable = new Subject(); private abortController?: AbortController; private dataView?: DataView; - private field?: DataViewField; + private field?: OptionsListField; private searchString = ''; // State to be passed down to component @@ -176,9 +177,9 @@ export class OptionsListEmbeddable extends Embeddable => { - const { dataViewId, fieldName } = this.getInput(); + const { dataViewId, fieldName, textFieldName } = this.getInput(); if (!this.dataView || this.dataView.id !== dataViewId) { this.dataView = await this.dataViewsService.get(dataViewId); if (this.dataView === undefined) { @@ -190,7 +191,10 @@ export class OptionsListEmbeddable extends Embeddable + i18n.translate('controls.optionsList.editor.runPastTimeout', { + defaultMessage: 'Run past timeout', + }), }, popover: { getLoadingMessage: () => diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_editor.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_editor.tsx index fa0c2c7d3cc45..13f688c5dd318 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_editor.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_editor.tsx @@ -24,7 +24,6 @@ import { RangeSliderStrings } from './range_slider_strings'; interface RangeSliderEditorState { dataViewListItems: DataViewListItem[]; dataView?: DataView; - fieldName?: string; } const FieldPicker = withSuspense(LazyFieldPicker, null); @@ -37,19 +36,20 @@ export const RangeSliderEditor = ({ setDefaultTitle, getRelevantDataViewId, setLastUsedDataViewId, + selectedField, + setSelectedField, }: ControlEditorProps) => { // Controls Services Context const { dataViews } = pluginServices.getHooks(); const { getIdsWithTitle, getDefaultId, get } = dataViews.useService(); const [state, setState] = useState({ - fieldName: initialInput?.fieldName, dataViewListItems: [], }); useMount(() => { let mounted = true; - if (state.fieldName) setDefaultTitle(state.fieldName); + if (selectedField) setDefaultTitle(selectedField); (async () => { const dataViewListItems = await getIdsWithTitle(); const initialId = @@ -68,11 +68,11 @@ export const RangeSliderEditor = ({ }); useEffect( - () => setValidState(Boolean(state.fieldName) && Boolean(state.dataView)), - [state.fieldName, setValidState, state.dataView] + () => setValidState(Boolean(selectedField) && Boolean(state.dataView)), + [selectedField, setValidState, state.dataView] ); - const { dataView, fieldName } = state; + const { dataView } = state; return ( <> @@ -84,7 +84,7 @@ export const RangeSliderEditor = ({ if (dataViewId === dataView?.id) return; onChange({ dataViewId }); - setState((s) => ({ ...s, fieldName: undefined })); + setSelectedField(undefined); get(dataViewId).then((newDataView) => { setState((s) => ({ ...s, dataView: newDataView })); }); @@ -97,12 +97,12 @@ export const RangeSliderEditor = ({ field.aggregatable && field.type === 'number'} - selectedFieldName={fieldName} + selectedFieldName={selectedField} dataView={dataView} onSelectField={(field) => { setDefaultTitle(field.displayName ?? field.name); onChange({ fieldName: field.name }); - setState((s) => ({ ...s, fieldName: field.name })); + setSelectedField(field.name); }} /> diff --git a/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx b/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx index 7ae7871497045..90ea07dc276bd 100644 --- a/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx +++ b/src/plugins/controls/public/control_types/time_slider/__stories__/time_slider.component.stories.tsx @@ -16,7 +16,7 @@ export default { description: '', }; -const TimeSliderWrapper: FC> = (props) => { +const TimeSliderWrapper: FC> = (props) => { const [value, setValue] = useState(props.value); const onChange = useCallback( (newValue: [number | null, number | null]) => { @@ -31,7 +31,13 @@ const TimeSliderWrapper: FC> = ( return (
    - +
    ); diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx index 89efce270d14c..1bb2f90b44121 100644 --- a/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx +++ b/src/plugins/controls/public/control_types/time_slider/time_slider.component.tsx @@ -70,6 +70,7 @@ export function getInterval(min: number, max: number, steps = 6): number { } export interface TimeSliderProps { + id: string; range?: [number | undefined, number | undefined]; value: [number | null, number | null]; onChange: (range: [number | null, number | null]) => void; @@ -167,10 +168,15 @@ export const TimeSlider: FC = (props) => { } const button = ( - - - -
    -
    - - -
    -
    - - - -
    - - - - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 20, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "showPerPageOptions": undefined, - "totalItemCount": 1, - } - } - responsive={true} - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" - > + class="euiSpacer euiSpacer--m css-hg1jdf-euiSpacer-m" + />
    -
    - -
    - -
    - -
    - - -
    - -
    - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
    -
    - - - -
    -
    -
    -
    -
    -
    -
    -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - column1 -
    -
    - -
    - -
    - 123 -
    -
    - -
    - -
    - -
    - -
    - -
    -
    -
    -
    - - -
    - -
    - - - -
    - -
    - - - - : - 20 - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
    -
    - - - -
    -
    -
    -
    -
    - -
    - - - -
    -
    -
    -
    -
    -
    - - + class="euiText euiText--medium" + > +

    + The element did not provide any data. +

    - - - - - + +
    +
    +
    + `; -exports[`Inspector Data View component should support multiple datatables 1`] = ` - + loading + } intl={ Object { @@ -1774,1436 +147,655 @@ exports[`Inspector Data View component should support multiple datatables 1`] = "timeZone": null, } } - title="Test Data" > - +
    + loading +
    + +`; + +exports[`Inspector Data View component should render single table without selector 1`] = ` +Array [ +
    +
    +
    +
    +
    + +
    +
    +
    +
    , +
    , +
    - +
    -
    +
    -
    -

    - - There are 2 tables in total - -

    + + + + Sorting + + + +
    - - +
    +
    +
    + + + + + + + + + + + +
    +
    -
    - - + + + column1 + + + +
    - + column1 + +
    - -
    - - - Selected: - - -
    -
    - +
    - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="inspectorTableChooser" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - repositionOnScroll={true} - > -
    -
    - - - -
    -
    -
    -
    - + class="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + /> +
    - - -
    - - +
    +
    +
    +
    +
    +
    - - + + + - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="inspectorDownloadData" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - repositionOnScroll={true} + Rows per page: 20 + + + +
    +
    +
    +
    +
    - + 1 + + + + + + + +
    - - +
    , +] +`; + +exports[`Inspector Data View component should support multiple datatables 1`] = ` +Array [ +
    +
    +

    + There are 2 tables in total +

    +
    +
    - - +
    + + Selected: + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    - +
    + +
    +
    +
    +
    , +
    , +
    +
    +
    - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 20, - "pageSizeOptions": Array [ - 10, - 20, - 50, - ], - "showPerPageOptions": undefined, - "totalItemCount": 1, - } - } - responsive={true} - sorting={ - Object { - "allowNeutralSort": true, - "sort": undefined, - } - } - tableLayout="fixed" +
    +
    -
    - +
    +
    - -
    - -
    - - + -
    - -
    - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
    -
    - - - -
    -
    -
    -
    -
    -
    -
    -
    - + Sorting + + +
    - - +
    +
    +
    +
    + + + + + + + + + + + +
    +
    + +
    - -
    - +
    +
    +
    + 123 +
    +
    - - - -
    - -
    - - - - : - 20 - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
    -
    - - - -
    -
    -
    -
    -
    - -
    - - - -
    -
    -
    -
    -
    +
    - - +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    - - - - - +
    +
    + +
    +
    +
    +
    , +] `; diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_view.test.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_view.test.tsx index 00817e3516720..08f5984ba9f6d 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/data_view.test.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_view.test.tsx @@ -63,7 +63,7 @@ describe('Inspector Data View', () => { adapters.tables.logDatatable({ columns: [{ id: '1' }], rows: [{ '1': 123 }] }); // After the loader has resolved we'll still need one update, to "flush" the state changes component.update(); - expect(component).toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); it('should render single table without selector', async () => { @@ -80,7 +80,7 @@ describe('Inspector Data View', () => { component.update(); expect(component.find('[data-test-subj="inspectorDataViewSelectorLabel"]')).toHaveLength(0); - expect(component).toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); it('should support multiple datatables', async () => { @@ -104,7 +104,7 @@ describe('Inspector Data View', () => { component.update(); expect(component.find('[data-test-subj="inspectorDataViewSelectorLabel"]')).toHaveLength(1); - expect(component).toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); }); }); diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap b/src/plugins/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap index 5d417dadca923..9e68c1b787b76 100644 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap @@ -1,224 +1,88 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ScriptingWarningCallOut should render normally 1`] = ` - - -
    -

    - - - , - "scriptsInAggregation": - - , - } - } +

    + + Familiarize yourself with + - - and - - - - before using this feature. Scripted fields can be used to display and aggregate calculated values. As such, they can be very slow and, if done incorrectly, can cause Kibana to become unusable. + scripted fields - -

    -
    -
    - + and + + before using this feature. Scripted fields can be used to display and aggregate calculated values. As such, they can be very slow and, if done incorrectly, can cause Kibana to become unusable. + +

    +
    , +
    , +
    - - +
    - - - - - Scripted fields are deprecated - - - -
    -
    - -
    - + + For greater flexibility and Painless script support, use + - - . - - -

    -
    - -
    - + + runtime fields + + + . + +

    - +
    - - -
    - - +
    , +
    , +] `; exports[`ScriptingWarningCallOut should render nothing if not visible 1`] = ` diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_call_outs/warning_call_out.test.tsx b/src/plugins/data_view_management/public/components/field_editor/components/scripting_call_outs/warning_call_out.test.tsx index 1233bb853f3a0..c06226cfc2521 100644 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_call_outs/warning_call_out.test.tsx +++ b/src/plugins/data_view_management/public/components/field_editor/components/scripting_call_outs/warning_call_out.test.tsx @@ -23,7 +23,7 @@ describe('ScriptingWarningCallOut', () => { }, }); - expect(component).toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); it('should render nothing if not visible', async () => { diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/__snapshots__/source.test.tsx.snap b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/__snapshots__/source.test.tsx.snap index 41b7ee37413d9..81bf42c51a2e0 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/__snapshots__/source.test.tsx.snap +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/__snapshots__/source.test.tsx.snap @@ -1,179 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Source Viewer component renders error state 1`] = ` - - - Could not fetch data at this time. Refresh the tab to try again. - - - Refresh - -
    - } - iconType="alert" - title={ -

    - An Error Occurred -

    - } +
    - + +
    +
    -
    + An Error Occurred + +
    - - - -
    + class="euiSpacer euiSpacer--m css-hg1jdf-euiSpacer-m" + />
    -
    - -

    - An Error Occurred -

    -
    - + Could not fetch data at this time. Refresh the tab to try again. +
    + - - -
    -
    - + Refresh + - +
    -
    +
    - - - +
    +
    +
    `; exports[`Source Viewer component renders json code editor 1`] = ` @@ -258,8 +150,91 @@ exports[`Source Viewer component renders json code editor 1`] = ` size="s" >
    + css="unknown styles" + > + + + + , + "ctr": 2, + "insertionPoint": undefined, + "isSpeedy": false, + "key": "css", + "nonce": undefined, + "prepend": undefined, + "tags": Array [ + , + , + ], + }, + } + } + isStringTag={true} + serialized={ + Object { + "map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL3NwYWNlci9zcGFjZXIuc3R5bGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQW9CUSIsImZpbGUiOiIuLi8uLi8uLi9zcmMvY29tcG9uZW50cy9zcGFjZXIvc3BhY2VyLnN0eWxlcy50cyIsInNvdXJjZXNDb250ZW50IjpbIi8qXG4gKiBDb3B5cmlnaHQgRWxhc3RpY3NlYXJjaCBCLlYuIGFuZC9vciBsaWNlbnNlZCB0byBFbGFzdGljc2VhcmNoIEIuVi4gdW5kZXIgb25lXG4gKiBvciBtb3JlIGNvbnRyaWJ1dG9yIGxpY2Vuc2UgYWdyZWVtZW50cy4gTGljZW5zZWQgdW5kZXIgdGhlIEVsYXN0aWMgTGljZW5zZVxuICogMi4wIGFuZCB0aGUgU2VydmVyIFNpZGUgUHVibGljIExpY2Vuc2UsIHYgMTsgeW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHRcbiAqIGluIGNvbXBsaWFuY2Ugd2l0aCwgYXQgeW91ciBlbGVjdGlvbiwgdGhlIEVsYXN0aWMgTGljZW5zZSAyLjAgb3IgdGhlIFNlcnZlclxuICogU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxLlxuICovXG5cbmltcG9ydCB7IGNzcyB9IGZyb20gJ0BlbW90aW9uL3JlYWN0JztcbmltcG9ydCB7IFVzZUV1aVRoZW1lIH0gZnJvbSAnLi4vLi4vc2VydmljZXMnO1xuXG5leHBvcnQgY29uc3QgZXVpU3BhY2VyU3R5bGVzID0gKHsgZXVpVGhlbWUgfTogVXNlRXVpVGhlbWUpID0+ICh7XG4gIC8vIGJhc2VcbiAgZXVpU3BhY2VyOiBjc3NgXG4gICAgZmxleC1zaHJpbms6IDA7IC8vIGRvbid0IGV2ZXIgbGV0IHRoaXMgc2hyaW5rIGluIGhlaWdodCBpZiBkaXJlY3QgZGVzY2VuZGVudCBvZiBmbGV4O1xuICBgLFxuICAvLyB2YXJpYW50c1xuICB4czogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnhzfTtcbiAgYCxcbiAgczogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnN9O1xuICBgLFxuICBtOiBjc3NgXG4gICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemUuYmFzZX07XG4gIGAsXG4gIGw6IGNzc2BcbiAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZS5sfTtcbiAgYCxcbiAgeGw6IGNzc2BcbiAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZS54bH07XG4gIGAsXG4gIHh4bDogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnh4bH07XG4gIGAsXG59KTtcbiJdfQ== */", + "name": "i2qclb-euiSpacer-s", + "next": undefined, + "styles": "flex-shrink:0;label:euiSpacer;;;height:8px;;label:s;;;", + "toString": [Function], + } + } + /> +
    +
    { /> ); - expect(comp.children()).toMatchSnapshot(); + expect(comp.children().render()).toMatchSnapshot(); const errorPrompt = comp.find(EuiEmptyPrompt); expect(errorPrompt.length).toBe(1); const refreshButton = comp.find(EuiButton); diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 88ff7f196f984..a6a276d440dfa 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -132,6 +132,37 @@ export abstract class Container< return this.createAndSaveEmbeddable(type, panelState); } + public async replaceEmbeddable< + EEI extends EmbeddableInput = EmbeddableInput, + EEO extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable + >(id: string, newExplicitInput: Partial, newType?: string) { + if (!this.input.panels[id]) { + throw new PanelNotFoundError(); + } + + if (newType && newType !== this.input.panels[id].type) { + const factory = this.getFactory(newType) as EmbeddableFactory | undefined; + if (!factory) { + throw new EmbeddableFactoryNotFoundError(newType); + } + this.updateInput({ + panels: { + ...this.input.panels, + [id]: { + ...this.input.panels[id], + explicitInput: { ...newExplicitInput, id }, + type: newType, + }, + }, + } as Partial); + } else { + this.updateInputForChild(id, newExplicitInput); + } + + await this.untilEmbeddableLoaded(id); + } + public removeEmbeddable(embeddableId: string) { // Just a shortcut for removing the panel from input state, all internal state will get cleaned up naturally // by the listener. diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index f082000b38d4b..5539f854b24d9 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -98,4 +98,14 @@ export interface IContainer< type: string, explicitInput: Partial ): Promise; + + replaceEmbeddable< + EEI extends EmbeddableInput = EmbeddableInput, + EEO extends EmbeddableOutput = EmbeddableOutput, + E extends Embeddable = Embeddable + >( + id: string, + newExplicitInput: Partial, + newType?: string + ): void; } diff --git a/src/plugins/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap index 2d850ee8082f9..7d957737284c3 100644 --- a/src/plugins/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap +++ b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap @@ -41,10 +41,10 @@ exports[`ViewApiRequestFlyout is rendered 1`] = `

    diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap
    index d970dd5416816..8527a9a109647 100644
    --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap
    +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap
    @@ -1,475 +1,125 @@
     // Jest Snapshot v1, https://goo.gl/fbAQLP
     
     exports[`bulkCreate should display error message when bulkCreate request fails 1`] = `
    -
    -  
    -    

    - Load Kibana objects -

    -
    - , +
    - -
    - -
    -

    - Imports index pattern, visualizations and pre-defined dashboards. -

    -
    -
    -
    -
    - +

    + Imports index pattern, visualizations and pre-defined dashboards. +

    +
    +
    +
    + - - -
    - + Load Kibana objects + + +
    - - -
    - - , +
    , +
    -
    - - Request failed, Error: simulated bulkRequest error - -
    + Request failed, Error: simulated bulkRequest error +
    - - +
    , +] `; exports[`bulkCreate should display success message when bulkCreate is successful 1`] = ` - - -

    - Load Kibana objects -

    -
    - , +
    - -
    - -
    -

    - Imports index pattern, visualizations and pre-defined dashboards. -

    -
    -
    -
    -
    - +

    + Imports index pattern, visualizations and pre-defined dashboards. +

    +
    +
    +
    + - - -
    - + Load Kibana objects + + +
    - - -
    - - , +
    , +
    -
    - - 1 saved objects successfully added - -
    + 1 saved objects successfully added +
    - - +
    , +] `; exports[`renders 1`] = ` diff --git a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js index 67ae2d1dd2eed..27dad0f378ab2 100644 --- a/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js +++ b/src/plugins/home/public/application/components/tutorial/saved_objects_installer.test.js @@ -45,7 +45,7 @@ describe('bulkCreate', () => { // Ensure the state changes are reflected component.update(); - expect(component).toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); test('should display error message when bulkCreate request fails', async () => { @@ -66,7 +66,7 @@ describe('bulkCreate', () => { // Ensure the state changes are reflected component.update(); - expect(component).toMatchSnapshot(); + expect(component.render()).toMatchSnapshot(); }); test('should filter out saved object version before calling bulkCreate', async () => { diff --git a/src/plugins/newsfeed/public/components/flyout_list.tsx b/src/plugins/newsfeed/public/components/flyout_list.tsx index 622ae287bd0c1..8abc0896fff4f 100644 --- a/src/plugins/newsfeed/public/components/flyout_list.tsx +++ b/src/plugins/newsfeed/public/components/flyout_list.tsx @@ -11,6 +11,7 @@ import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, + EuiFlyoutProps, EuiTitle, EuiLink, EuiFlyoutFooter, @@ -28,13 +29,14 @@ import { NewsfeedItem } from '../types'; import { NewsEmptyPrompt } from './empty_news'; import { NewsLoadingPrompt } from './loading_news'; -export const NewsfeedFlyout = () => { +export const NewsfeedFlyout = (props: Partial) => { const { newsFetchResult, setFlyoutVisible } = useContext(NewsfeedContext); const closeFlyout = useCallback(() => setFlyoutVisible(false), [setFlyoutVisible]); return ( { return newsFetchResult ? newsFetchResult.hasNew : false; }, [newsFetchResult]); + const buttonRef = useRef(null); + const setButtonRef = (node: HTMLButtonElement | null) => (buttonRef.current = node); + useEffect(() => { const subscription = newsfeedApi.fetchResults$.subscribe((results) => { setNewsFetchResult(results); @@ -49,6 +52,7 @@ export const NewsfeedNavButton = ({ newsfeedApi }: Props) => { <> { > - {flyoutVisible ? : null} + {flyoutVisible ? : null} ); diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx index b47e425f67bb2..3c8a084f2686b 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx @@ -40,7 +40,7 @@ export const FieldPicker = ({ dataView.fields .filter( (f) => - f.name.includes(nameFilter) && + f.name.toLowerCase().includes(nameFilter.toLowerCase()) && (typesFilter.length === 0 || typesFilter.includes(f.type as string)) ) .filter((f) => (filterPredicate ? filterPredicate(f) : true)), diff --git a/src/plugins/presentation_util/public/components/redux_embeddables/redux_embeddable_wrapper.tsx b/src/plugins/presentation_util/public/components/redux_embeddables/redux_embeddable_wrapper.tsx index 7b7ac93c14a97..dc98b098f8428 100644 --- a/src/plugins/presentation_util/public/components/redux_embeddables/redux_embeddable_wrapper.tsx +++ b/src/plugins/presentation_util/public/components/redux_embeddables/redux_embeddable_wrapper.tsx @@ -94,6 +94,7 @@ export const ReduxEmbeddableWrapper = = ReduxEmbeddableContextServices & { containerActions: Pick< IContainer, - 'untilEmbeddableLoaded' | 'removeEmbeddable' | 'addNewEmbeddable' | 'updateInputForChild' + | 'untilEmbeddableLoaded' + | 'removeEmbeddable' + | 'addNewEmbeddable' + | 'updateInputForChild' + | 'replaceEmbeddable' >; }; 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 233e38874e6da..ef6102571f324 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 @@ -59,6 +59,25 @@ Object { "type": "expression", }, ], + "labelFont": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "align": Array [ + "center", + ], + "size": Array [ + "14", + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ], "percentageMode": Array [ true, ], @@ -133,6 +152,25 @@ Object { "type": "expression", }, ], + "labelFont": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "align": Array [ + "center", + ], + "size": Array [ + "14", + ], + }, + "function": "font", + "type": "function", + }, + ], + "type": "expression", + }, + ], "showLabels": Array [ false, ], diff --git a/src/plugins/vis_types/metric/public/to_ast.ts b/src/plugins/vis_types/metric/public/to_ast.ts index 322ea561abeb4..d206d046cde6a 100644 --- a/src/plugins/vis_types/metric/public/to_ast.ts +++ b/src/plugins/vis_types/metric/public/to_ast.ts @@ -83,6 +83,8 @@ export const toExpressionAst: VisToExpressionAst = (vis, params) => { ) ); + metricVis.addArgument('labelFont', buildExpression(`font size="14" align="center"`)); + if (colorsRange && colorsRange.length > 1) { const stopsWithColors = getStopsWithColorsFromRanges(colorsRange, colorSchema, invertColors); const palette = buildExpressionFunction('palette', { diff --git a/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts b/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts index 13ef3a248a583..ba25fcfce98e2 100644 --- a/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts +++ b/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts @@ -7,6 +7,7 @@ */ import expect from '@kbn/expect'; +import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -37,19 +38,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await timePicker.setDefaultDataRange(); // populate an initial set of controls and get their ids. - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'animal.keyword', title: 'Animal', }); - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'name.keyword', title: 'Animal Name', }); - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'sound.keyword', title: 'Animal Sound', diff --git a/test/functional/apps/dashboard_elements/controls/control_group_settings.ts b/test/functional/apps/dashboard_elements/controls/control_group_settings.ts index 3ca09bba99cea..de3a70b6a49d2 100644 --- a/test/functional/apps/dashboard_elements/controls/control_group_settings.ts +++ b/test/functional/apps/dashboard_elements/controls/control_group_settings.ts @@ -7,6 +7,7 @@ */ import expect from '@kbn/expect'; +import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -29,7 +30,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('adjust layout of controls', async () => { await dashboard.switchToEditMode(); - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'sound.keyword', }); @@ -41,7 +43,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('apply new default size', async () => { it('to new controls only', async () => { await dashboardControls.updateControlsSize('medium'); - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'name.keyword', }); @@ -54,7 +57,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('to all existing controls', async () => { - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'animal.keyword', width: 'large', @@ -82,7 +86,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('when at least one control', async () => { - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'sound.keyword', }); diff --git a/test/functional/apps/dashboard_elements/controls/controls_callout.ts b/test/functional/apps/dashboard_elements/controls/controls_callout.ts index fc6316940c8a4..73bf8fb50c241 100644 --- a/test/functional/apps/dashboard_elements/controls/controls_callout.ts +++ b/test/functional/apps/dashboard_elements/controls/controls_callout.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common'; + import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -47,7 +49,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('adding control hides the empty control callout', async () => { - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'sound.keyword', }); diff --git a/test/functional/apps/dashboard_elements/controls/index.ts b/test/functional/apps/dashboard_elements/controls/index.ts index f5ec41d593995..c867f00b32e82 100644 --- a/test/functional/apps/dashboard_elements/controls/index.ts +++ b/test/functional/apps/dashboard_elements/controls/index.ts @@ -51,5 +51,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid loadTestFile(require.resolve('./options_list')); loadTestFile(require.resolve('./range_slider')); loadTestFile(require.resolve('./control_group_chaining')); + loadTestFile(require.resolve('./replace_controls')); }); } diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index 09584dd7f6a51..17a028a39464e 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -27,8 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'header', ]); - // FAILING: https://github.com/elastic/kibana/issues/132049 - describe.skip('Dashboard options list integration', () => { + describe('Dashboard options list integration', () => { before(async () => { await common.navigateToApp('dashboard'); await dashboard.gotoDashboardLandingPage(); @@ -58,7 +58,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('selects the last used data view by default', async () => { - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'sound.keyword', }); @@ -71,7 +72,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Options List Control creation and editing experience', async () => { it('can add a new options list control from a blank state', async () => { - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'logstash-*', fieldName: 'machine.os.raw', }); @@ -79,7 +81,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('can add a second options list control with a non-default data view', async () => { - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'sound.keyword', }); @@ -187,7 +190,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie'); - await dashboardControls.createOptionsListControl({ + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, dataViewTitle: 'animals-*', fieldName: 'sound.keyword', title: 'Animal Sounds', @@ -267,7 +271,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Selections made in control apply to dashboard', async () => { it('Shows available options in options list', async () => { - await ensureAvailableOptionsEql(allAvailableOptions); + await queryBar.setQuery(''); + await queryBar.submitQuery(); + await dashboard.waitForRenderComplete(); + await header.waitUntilLoadingHasFinished(); + await retry.try(async () => { + await ensureAvailableOptionsEql(allAvailableOptions); + }); }); it('Can search options list for available options', async () => { @@ -278,6 +288,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); }); + it('Can search options list for available options case insensitive', async () => { + await dashboardControls.optionsListOpenPopover(controlId); + await dashboardControls.optionsListPopoverSearchForOption('MEO'); + await ensureAvailableOptionsEql(['meow'], true); + await dashboardControls.optionsListPopoverClearSearch(); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); + }); + it('Can select multiple available options', async () => { await dashboardControls.optionsListOpenPopover(controlId); await dashboardControls.optionsListPopoverSelectOption('hiss'); @@ -305,9 +323,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId); expect(selectionString).to.be('hiss, grr'); - }); - after(async () => { await dashboardControls.optionsListOpenPopover(controlId); await dashboardControls.optionsListPopoverClearSelections(); await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); diff --git a/test/functional/apps/dashboard_elements/controls/range_slider.ts b/test/functional/apps/dashboard_elements/controls/range_slider.ts index 1ce8f05cae190..b2d07e7a49489 100644 --- a/test/functional/apps/dashboard_elements/controls/range_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/range_slider.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -80,7 +81,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('create and edit', async () => { it('can create a new range slider control from a blank state', async () => { - await dashboardControls.createRangeSliderControl({ + await dashboardControls.createControl({ + controlType: RANGE_SLIDER_CONTROL, dataViewTitle: 'logstash-*', fieldName: 'bytes', width: 'small', @@ -89,7 +91,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('can add a second range list control with a non-default data view', async () => { - await dashboardControls.createRangeSliderControl({ + await dashboardControls.createControl({ + controlType: RANGE_SLIDER_CONTROL, dataViewTitle: 'kibana_sample_data_flights', fieldName: 'AvgTicketPrice', width: 'medium', @@ -204,7 +207,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('disables inputs when no data available', async () => { - await dashboardControls.createRangeSliderControl({ + await dashboardControls.createControl({ + controlType: RANGE_SLIDER_CONTROL, dataViewTitle: 'logstash-*', fieldName: 'bytes', width: 'small', diff --git a/test/functional/apps/dashboard_elements/controls/replace_controls.ts b/test/functional/apps/dashboard_elements/controls/replace_controls.ts new file mode 100644 index 0000000000000..f6af399905077 --- /dev/null +++ b/test/functional/apps/dashboard_elements/controls/replace_controls.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { + OPTIONS_LIST_CONTROL, + RANGE_SLIDER_CONTROL, + TIME_SLIDER_CONTROL, +} from '@kbn/controls-plugin/common'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + const { dashboardControls, timePicker, common, dashboard } = getPageObjects([ + 'dashboardControls', + 'timePicker', + 'dashboard', + 'common', + 'header', + ]); + + const changeFieldType = async (newField: string) => { + const saveButton = await testSubjects.find('control-editor-save'); + expect(await saveButton.isEnabled()).to.be(false); + await dashboardControls.controlsEditorSetfield(newField); + expect(await saveButton.isEnabled()).to.be(true); + await dashboardControls.controlEditorSave(); + }; + + const replaceWithOptionsList = async (controlId: string) => { + await dashboardControls.controlEditorSetType(OPTIONS_LIST_CONTROL); + await changeFieldType('sound.keyword'); + await testSubjects.waitForEnabled(`optionsList-control-${controlId}`); + await dashboardControls.verifyControlType(controlId, 'optionsList-control'); + }; + + const replaceWithRangeSlider = async (controlId: string) => { + await dashboardControls.controlEditorSetType(RANGE_SLIDER_CONTROL); + await changeFieldType('weightLbs'); + await retry.try(async () => { + await dashboardControls.rangeSliderWaitForLoading(); + await dashboardControls.verifyControlType(controlId, 'range-slider-control'); + }); + }; + + const replaceWithTimeSlider = async (controlId: string) => { + await dashboardControls.controlEditorSetType(TIME_SLIDER_CONTROL); + await changeFieldType('@timestamp'); + await testSubjects.waitForDeleted('timeSlider-loading-spinner'); + await dashboardControls.verifyControlType(controlId, 'timeSlider'); + }; + + describe('Replacing controls', async () => { + let controlId: string; + + before(async () => { + await common.navigateToApp('dashboard'); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + await timePicker.setDefaultDataRange(); + }); + + describe('Replace options list', async () => { + beforeEach(async () => { + await dashboardControls.clearAllControls(); + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, + dataViewTitle: 'animals-*', + fieldName: 'sound.keyword', + }); + controlId = (await dashboardControls.getAllControlIds())[0]; + await dashboardControls.editExistingControl(controlId); + }); + + it('with range slider', async () => { + await replaceWithRangeSlider(controlId); + }); + + /** Because the time slider is temporarily disabled as of https://github.com/elastic/kibana/pull/130978, + ** I simply skipped all time slider tests for now :) **/ + it.skip('with time slider', async () => { + await replaceWithTimeSlider(controlId); + }); + }); + + describe('Replace range slider', async () => { + beforeEach(async () => { + await dashboardControls.clearAllControls(); + await dashboardControls.createControl({ + controlType: RANGE_SLIDER_CONTROL, + dataViewTitle: 'animals-*', + fieldName: 'weightLbs', + }); + await dashboardControls.rangeSliderWaitForLoading(); + controlId = (await dashboardControls.getAllControlIds())[0]; + await dashboardControls.editExistingControl(controlId); + }); + + it('with options list', async () => { + await replaceWithOptionsList(controlId); + }); + + it.skip('with time slider', async () => { + await replaceWithTimeSlider(controlId); + }); + }); + + describe.skip('Replace time slider', async () => { + beforeEach(async () => { + await dashboardControls.clearAllControls(); + await dashboardControls.createControl({ + controlType: TIME_SLIDER_CONTROL, + dataViewTitle: 'animals-*', + fieldName: '@timestamp', + }); + await testSubjects.waitForDeleted('timeSlider-loading-spinner'); + controlId = (await dashboardControls.getAllControlIds())[0]; + await dashboardControls.editExistingControl(controlId); + }); + + it('with options list', async () => { + await replaceWithOptionsList(controlId); + }); + + it('with range slider', async () => { + await replaceWithRangeSlider(controlId); + }); + }); + }); +} diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts index 179e27d09cf55..5d3b4d3a2b6cd 100644 --- a/test/functional/page_objects/dashboard_page_controls.ts +++ b/test/functional/page_objects/dashboard_page_controls.ts @@ -7,11 +7,7 @@ */ import expect from '@kbn/expect'; -import { - OPTIONS_LIST_CONTROL, - ControlWidth, - RANGE_SLIDER_CONTROL, -} from '@kbn/controls-plugin/common'; +import { OPTIONS_LIST_CONTROL, ControlWidth } from '@kbn/controls-plugin/common'; import { ControlGroupChainingSystem } from '@kbn/controls-plugin/common/control_group/types'; import { WebElementWrapper } from '../services/lib/web_element_wrapper'; @@ -82,7 +78,7 @@ export class DashboardPageControls extends FtrService { await this.retry.try(async () => { await this.testSubjects.existOrFail('control-editor-flyout'); }); - await this.testSubjects.click(`create-${type}-control`); + await this.controlEditorSetType(type); } /* ----------------------------------------------------------- @@ -234,6 +230,30 @@ export class DashboardPageControls extends FtrService { return controlElement; } + public async createControl({ + controlType, + dataViewTitle, + fieldName, + width, + title, + }: { + controlType: string; + title?: string; + fieldName: string; + width?: ControlWidth; + dataViewTitle?: string; + }) { + this.log.debug(`Creating ${controlType} control ${title ?? fieldName}`); + await this.openCreateControlFlyout(controlType); + + if (dataViewTitle) await this.controlsEditorSetDataView(dataViewTitle); + if (fieldName) await this.controlsEditorSetfield(fieldName); + if (title) await this.controlEditorSetTitle(title); + if (width) await this.controlEditorSetWidth(width); + + await this.controlEditorSave(); + } + public async hoverOverExistingControl(controlId: string) { const elementToHover = await this.getControlElementById(controlId); await this.retry.try(async () => { @@ -263,6 +283,14 @@ export class DashboardPageControls extends FtrService { await this.common.clickConfirmOnModal(); } + public async verifyControlType(controlId: string, expectedType: string) { + const controlButton = await this.find.byXPath( + `//div[@id='controlFrame--${controlId}']//button` + ); + const testSubj = await controlButton.getAttribute('data-test-subj'); + expect(testSubj).to.equal(`${expectedType}-${controlId}`); + } + // Options list functions public async optionsListGetSelectionsString(controlId: string) { this.log.debug(`Getting selections string for Options List: ${controlId}`); @@ -376,29 +404,12 @@ export class DashboardPageControls extends FtrService { await this.testSubjects.click(`field-picker-select-${fieldName}`); } - // Options List editor functions - public async createOptionsListControl({ - dataViewTitle, - fieldName, - width, - title, - }: { - title?: string; - fieldName: string; - width?: ControlWidth; - dataViewTitle?: string; - }) { - this.log.debug(`Creating options list control ${title ?? fieldName}`); - await this.openCreateControlFlyout(OPTIONS_LIST_CONTROL); - - if (dataViewTitle) await this.controlsEditorSetDataView(dataViewTitle); - if (fieldName) await this.controlsEditorSetfield(fieldName); - if (title) await this.controlEditorSetTitle(title); - if (width) await this.controlEditorSetWidth(width); - - await this.controlEditorSave(); + public async controlEditorSetType(type: string) { + this.log.debug(`Setting control type to ${type}`); + await this.testSubjects.click(`create-${type}-control`); } + // Options List editor functions public async optionsListEditorGetCurrentDataView(openAndCloseFlyout?: boolean) { if (openAndCloseFlyout) { await this.openCreateControlFlyout(OPTIONS_LIST_CONTROL); @@ -476,27 +487,4 @@ export class DashboardPageControls extends FtrService { await this.rangeSliderPopoverAssertOpen(); await this.testSubjects.click('rangeSlider__clearRangeButton'); } - - // Range slider editor functions - public async createRangeSliderControl({ - dataViewTitle, - fieldName, - width, - title, - }: { - title?: string; - fieldName: string; - width?: ControlWidth; - dataViewTitle?: string; - }) { - this.log.debug(`Creating range slider control ${title ?? fieldName}`); - await this.openCreateControlFlyout(RANGE_SLIDER_CONTROL); - - if (dataViewTitle) await this.controlsEditorSetDataView(dataViewTitle); - if (fieldName) await this.controlsEditorSetfield(fieldName); - if (title) await this.controlEditorSetTitle(title); - if (width) await this.controlEditorSetWidth(width); - - await this.controlEditorSave(); - } } diff --git a/test/package/roles/install_kibana_docker/tasks/main.yml b/test/package/roles/install_kibana_docker/tasks/main.yml index 2b0b70de30b6c..01dcf9f00bcce 100644 --- a/test/package/roles/install_kibana_docker/tasks/main.yml +++ b/test/package/roles/install_kibana_docker/tasks/main.yml @@ -24,3 +24,4 @@ ELASTICSEARCH_HOSTS: http://192.168.56.1:9200 ELASTICSEARCH_USERNAME: '{{ elasticsearch_username }}' ELASTICSEARCH_PASSWORD: '{{ elasticsearch_password }}' + XPACK_REPORTING_CAPTURE_BROWSER_CHROMIUM_DISABLESANDBOX: 'true' diff --git a/tsconfig.base.json b/tsconfig.base.json index 78023a603276a..daf7bf78903c1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -277,6 +277,8 @@ "@kbn/ui-actions-enhanced-examples-plugin/*": ["x-pack/examples/ui_actions_enhanced_examples/*"], "@kbn/actions-plugin": ["x-pack/plugins/actions"], "@kbn/actions-plugin/*": ["x-pack/plugins/actions/*"], + "@kbn/aiops-plugin": ["x-pack/plugins/aiops"], + "@kbn/aiops-plugin/*": ["x-pack/plugins/aiops/*"], "@kbn/alerting-plugin": ["x-pack/plugins/alerting"], "@kbn/alerting-plugin/*": ["x-pack/plugins/alerting/*"], "@kbn/apm-plugin": ["x-pack/plugins/apm"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index b1464f5cfbe2e..738c5242813be 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -37,6 +37,7 @@ "xpack.logstash": ["plugins/logstash"], "xpack.main": "legacy/plugins/xpack_main", "xpack.maps": ["plugins/maps"], + "xpack.aiops": ["plugins/aiops"], "xpack.ml": ["plugins/ml"], "xpack.monitoring": ["plugins/monitoring"], "xpack.osquery": ["plugins/osquery"], diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/connector_token_client.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/connector_token_client.test.ts index 54765b9e01b8f..dfa307ca3cd91 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/connector_token_client.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/connector_token_client.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import sinon from 'sinon'; import { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { ConnectorTokenClient } from './connector_token_client'; @@ -23,7 +24,13 @@ const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); let connectorTokenClient: ConnectorTokenClient; +let clock: sinon.SinonFakeTimers; + +beforeAll(() => { + clock = sinon.useFakeTimers(new Date('2021-01-01T12:00:00.000Z')); +}); beforeEach(() => { + clock.reset(); jest.resetAllMocks(); connectorTokenClient = new ConnectorTokenClient({ unsecuredSavedObjectsClient, @@ -31,6 +38,7 @@ beforeEach(() => { logger, }); }); +afterAll(() => clock.restore()); describe('create()', () => { test('creates connector_token with all given properties', async () => { @@ -131,7 +139,7 @@ describe('get()', () => { expect(result).toEqual({ connectorToken: null, hasErrors: false }); }); - test('return null and log the error if unsecuredSavedObjectsClient thows an error', async () => { + test('return null and log the error if unsecuredSavedObjectsClient throws an error', async () => { unsecuredSavedObjectsClient.find.mockRejectedValueOnce(new Error('Fail')); const result = await connectorTokenClient.get({ @@ -145,7 +153,7 @@ describe('get()', () => { expect(result).toEqual({ connectorToken: null, hasErrors: true }); }); - test('return null and log the error if encryptedSavedObjectsClient decrypt method thows an error', async () => { + test('return null and log the error if encryptedSavedObjectsClient decrypt method throws an error', async () => { const expectedResult = { total: 1, per_page: 10, @@ -178,6 +186,47 @@ describe('get()', () => { ]); expect(result).toEqual({ connectorToken: null, hasErrors: true }); }); + + test('return null and log the error if expiresAt is NaN', async () => { + const expectedResult = { + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: '1', + type: 'connector_token', + attributes: { + connectorId: '123', + tokenType: 'access_token', + createdAt: new Date().toISOString(), + expiresAt: 'yo', + }, + score: 1, + references: [], + }, + ], + }; + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '1', + type: 'connector_token', + references: [], + attributes: { + token: 'testtokenvalue', + }, + }); + + const result = await connectorTokenClient.get({ + connectorId: '123', + tokenType: 'access_token', + }); + + expect(logger.error.mock.calls[0]).toMatchObject([ + `Failed to get connector_token for connectorId "123" and tokenType: "access_token". Error: expiresAt is not a valid Date "yo"`, + ]); + expect(result).toEqual({ connectorToken: null, hasErrors: true }); + }); }); describe('update()', () => { @@ -375,12 +424,60 @@ describe('updateOrReplace()', () => { connectorId: '1', token: null, newToken: 'newToken', + tokenRequestDate: undefined as unknown as number, expiresInSec: 1000, deleteExisting: false, }); expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect((unsecuredSavedObjectsClient.create.mock.calls[0][1] as ConnectorToken).token).toBe( - 'newToken' + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'connector_token', + { + connectorId: '1', + createdAt: '2021-01-01T12:00:00.000Z', + expiresAt: '2021-01-01T12:16:40.000Z', + token: 'newToken', + tokenType: 'access_token', + updatedAt: '2021-01-01T12:00:00.000Z', + }, + { id: 'mock-saved-object-id' } + ); + + expect(unsecuredSavedObjectsClient.find).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.delete).not.toHaveBeenCalled(); + }); + + test('uses tokenRequestDate to determine expire time if provided', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'connector_token', + attributes: { + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + expiresAt: new Date('2021-01-01T08:00:00.000Z').toISOString(), + }, + references: [], + }); + await connectorTokenClient.updateOrReplace({ + connectorId: '1', + token: null, + newToken: 'newToken', + tokenRequestDate: new Date('2021-03-03T00:00:00.000Z').getTime(), + expiresInSec: 1000, + deleteExisting: false, + }); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'connector_token', + { + connectorId: '1', + createdAt: '2021-01-01T12:00:00.000Z', + expiresAt: '2021-03-03T00:16:40.000Z', + token: 'newToken', + tokenType: 'access_token', + updatedAt: '2021-01-01T12:00:00.000Z', + }, + { id: 'mock-saved-object-id' } ); expect(unsecuredSavedObjectsClient.find).not.toHaveBeenCalled(); @@ -434,6 +531,7 @@ describe('updateOrReplace()', () => { connectorId: '1', token: null, newToken: 'newToken', + tokenRequestDate: Date.now(), expiresInSec: 1000, deleteExisting: true, }); @@ -483,6 +581,7 @@ describe('updateOrReplace()', () => { expiresAt: new Date().toISOString(), }, newToken: 'newToken', + tokenRequestDate: Date.now(), expiresInSec: 1000, deleteExisting: true, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/connector_token_client.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/connector_token_client.ts index 6ce91fad94546..df1615d503329 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/connector_token_client.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/connector_token_client.ts @@ -38,6 +38,7 @@ interface UpdateOrReplaceOptions { token: ConnectorToken | null; newToken: string; expiresInSec: number; + tokenRequestDate: number; deleteExisting: boolean; } export class ConnectorTokenClient { @@ -195,6 +196,7 @@ export class ConnectorTokenClient { return { hasErrors: false, connectorToken: null }; } + let accessToken: string; try { const { attributes: { token }, @@ -203,14 +205,7 @@ export class ConnectorTokenClient { connectorTokensResult[0].id ); - return { - hasErrors: false, - connectorToken: { - id: connectorTokensResult[0].id, - ...connectorTokensResult[0].attributes, - token, - }, - }; + accessToken = token; } catch (err) { this.logger.error( `Failed to decrypt connector_token for connectorId "${connectorId}" and tokenType: "${ @@ -219,6 +214,24 @@ export class ConnectorTokenClient { ); return { hasErrors: true, connectorToken: null }; } + + if (isNaN(Date.parse(connectorTokensResult[0].attributes.expiresAt))) { + this.logger.error( + `Failed to get connector_token for connectorId "${connectorId}" and tokenType: "${ + tokenType ?? 'access_token' + }". Error: expiresAt is not a valid Date "${connectorTokensResult[0].attributes.expiresAt}"` + ); + return { hasErrors: true, connectorToken: null }; + } + + return { + hasErrors: false, + connectorToken: { + id: connectorTokensResult[0].id, + ...connectorTokensResult[0].attributes, + token: accessToken, + }, + }; } /** @@ -258,9 +271,11 @@ export class ConnectorTokenClient { token, newToken, expiresInSec, + tokenRequestDate, deleteExisting, }: UpdateOrReplaceOptions) { expiresInSec = expiresInSec ?? 3600; + tokenRequestDate = tokenRequestDate ?? Date.now(); if (token === null) { if (deleteExisting) { await this.deleteConnectorTokens({ @@ -272,14 +287,14 @@ export class ConnectorTokenClient { await this.create({ connectorId, token: newToken, - expiresAtMillis: new Date(Date.now() + expiresInSec * 1000).toISOString(), + expiresAtMillis: new Date(tokenRequestDate + expiresInSec * 1000).toISOString(), tokenType: 'access_token', }); } else { await this.update({ id: token.id!.toString(), token: newToken, - expiresAtMillis: new Date(Date.now() + expiresInSec * 1000).toISOString(), + expiresAtMillis: new Date(tokenRequestDate + expiresInSec * 1000).toISOString(), tokenType: 'access_token', }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts index 2efa79cf09c48..c3464a11e557e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import sinon from 'sinon'; import { Logger } from '@kbn/core/server'; import { asyncForEach } from '@kbn/std'; import { loggingSystemMock } from '@kbn/core/server/mocks'; @@ -20,7 +21,15 @@ const logger = loggingSystemMock.create().get() as jest.Mocked; const configurationUtilities = actionsConfigMock.create(); const connectorTokenClient = connectorTokenClientMock.create(); +let clock: sinon.SinonFakeTimers; + describe('getOAuthClientCredentialsAccessToken', () => { + beforeAll(() => { + clock = sinon.useFakeTimers(new Date('2021-01-01T12:00:00.000Z')); + }); + beforeEach(() => clock.reset()); + afterAll(() => clock.restore()); + const getOAuthClientCredentialsAccessTokenOpts = { connectorId: '123', logger, @@ -52,8 +61,8 @@ describe('getOAuthClientCredentialsAccessToken', () => { connectorId: '123', tokenType: 'access_token', token: 'testtokenvalue', - createdAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 10000000000).toISOString(), + createdAt: new Date('2021-01-01T08:00:00.000Z').toISOString(), + expiresAt: new Date('2021-01-02T13:00:00.000Z').toISOString(), }, }); const accessToken = await getOAuthClientCredentialsAccessToken( @@ -95,14 +104,15 @@ describe('getOAuthClientCredentialsAccessToken', () => { connectorId: '123', token: null, newToken: 'access_token brandnewaccesstoken', + tokenRequestDate: 1609502400000, expiresInSec: 1000, deleteExisting: false, }); }); test('creates new assertion if stored access token exists but is expired', async () => { - const createdAt = new Date().toISOString(); - const expiresAt = new Date(Date.now() - 100).toISOString(); + const createdAt = new Date('2021-01-01T08:00:00.000Z').toISOString(); + const expiresAt = new Date('2021-01-01T09:00:00.000Z').toISOString(); connectorTokenClient.get.mockResolvedValueOnce({ hasErrors: false, connectorToken: { @@ -147,6 +157,7 @@ describe('getOAuthClientCredentialsAccessToken', () => { expiresAt, }, newToken: 'access_token brandnewaccesstoken', + tokenRequestDate: 1609502400000, expiresInSec: 1000, deleteExisting: false, }); @@ -210,6 +221,7 @@ describe('getOAuthClientCredentialsAccessToken', () => { (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ tokenType: 'access_token', accessToken: 'brandnewaccesstoken', + tokenRequestDate: 1609502400000, expiresIn: 1000, }); connectorTokenClient.updateOrReplace.mockRejectedValueOnce(new Error('updateOrReplace error')); @@ -268,6 +280,7 @@ describe('getOAuthClientCredentialsAccessToken', () => { (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ tokenType: 'access_token', accessToken: 'brandnewaccesstoken', + tokenRequestDate: 1609502400000, expiresIn: 1000, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts index 803cce2db7668..1cce245a154c2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts @@ -62,6 +62,9 @@ export const getOAuthClientCredentialsAccessToken = async ({ } if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { + // Save the time before requesting token so we can use it to calculate expiration + const requestTokenStart = Date.now(); + // request access token with jwt assertion const tokenResult = await requestOAuthClientCredentialsToken( tokenUrl, @@ -82,6 +85,7 @@ export const getOAuthClientCredentialsAccessToken = async ({ connectorId, token: connectorToken, newToken: accessToken, + tokenRequestDate: requestTokenStart, expiresInSec: tokenResult.expiresIn, deleteExisting: hasErrors, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts index b48456ddd2a8c..0fe837fc0581a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import sinon from 'sinon'; import { Logger } from '@kbn/core/server'; import { asyncForEach } from '@kbn/std'; import { loggingSystemMock } from '@kbn/core/server/mocks'; @@ -24,7 +25,15 @@ const logger = loggingSystemMock.create().get() as jest.Mocked; const configurationUtilities = actionsConfigMock.create(); const connectorTokenClient = connectorTokenClientMock.create(); +let clock: sinon.SinonFakeTimers; + describe('getOAuthJwtAccessToken', () => { + beforeAll(() => { + clock = sinon.useFakeTimers(new Date('2021-01-01T12:00:00.000Z')); + }); + beforeEach(() => clock.reset()); + afterAll(() => clock.restore()); + const getOAuthJwtAccessTokenOpts = { connectorId: '123', logger, @@ -58,8 +67,8 @@ describe('getOAuthJwtAccessToken', () => { connectorId: '123', tokenType: 'access_token', token: 'testtokenvalue', - createdAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 10000000000).toISOString(), + createdAt: new Date('2021-01-01T08:00:00.000Z').toISOString(), + expiresAt: new Date('2021-01-02T13:00:00.000Z').toISOString(), }, }); const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); @@ -105,14 +114,15 @@ describe('getOAuthJwtAccessToken', () => { connectorId: '123', token: null, newToken: 'access_token brandnewaccesstoken', + tokenRequestDate: 1609502400000, expiresInSec: 1000, deleteExisting: false, }); }); test('creates new assertion if stored access token exists but is expired', async () => { - const createdAt = new Date().toISOString(); - const expiresAt = new Date(Date.now() - 100).toISOString(); + const createdAt = new Date('2021-01-01T08:00:00.000Z').toISOString(); + const expiresAt = new Date('2021-01-01T09:00:00.000Z').toISOString(); connectorTokenClient.get.mockResolvedValueOnce({ hasErrors: false, connectorToken: { @@ -161,6 +171,7 @@ describe('getOAuthJwtAccessToken', () => { createdAt, expiresAt, }, + tokenRequestDate: 1609502400000, newToken: 'access_token brandnewaccesstoken', expiresInSec: 1000, deleteExisting: false, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts index a4867d99556e7..1233a61c0f3c8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts @@ -72,6 +72,9 @@ export const getOAuthJwtAccessToken = async ({ keyId: jwtKeyId, }); + // Save the time before requesting token so we can use it to calculate expiration + const requestTokenStart = Date.now(); + // request access token with jwt assertion const tokenResult = await requestOAuthJWTToken( tokenUrl, @@ -92,6 +95,7 @@ export const getOAuthJwtAccessToken = async ({ connectorId, token: connectorToken, newToken: accessToken, + tokenRequestDate: requestTokenStart, expiresInSec: tokenResult.expiresIn, deleteExisting: hasErrors, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index fbf0d90541659..fe6fc3492492a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -5,10 +5,21 @@ * 2.0. */ +import axios from 'axios'; +import { Logger } from '@kbn/core/server'; +import { sendEmail } from './send_email'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import nodemailer from 'nodemailer'; +import { ProxySettings } from '../../types'; +import { actionsConfigMock } from '../../actions_config.mock'; +import { CustomHostSettings } from '../../config'; +import { sendEmailGraphApi } from './send_email_graph_api'; +import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token'; +import { connectorTokenClientMock } from './connector_token_client.mock'; + jest.mock('nodemailer', () => ({ createTransport: jest.fn(), })); - jest.mock('./send_email_graph_api', () => ({ sendEmailGraphApi: jest.fn(), })); @@ -16,36 +27,32 @@ jest.mock('./get_oauth_client_credentials_access_token', () => ({ getOAuthClientCredentialsAccessToken: jest.fn(), })); -import { Logger } from '@kbn/core/server'; -import { sendEmail } from './send_email'; -import { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; -import nodemailer from 'nodemailer'; -import { ProxySettings } from '../../types'; -import { actionsConfigMock } from '../../actions_config.mock'; -import { CustomHostSettings } from '../../config'; -import { sendEmailGraphApi } from './send_email_graph_api'; -import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token'; -import { ConnectorTokenClient } from './connector_token_client'; -import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; +jest.mock('axios'); +const mockAxiosInstanceInterceptor = { + request: { eject: jest.fn(), use: jest.fn() }, + response: { eject: jest.fn(), use: jest.fn() }, +}; const createTransportMock = nodemailer.createTransport as jest.Mock; const sendMailMockResult = { result: 'does not matter' }; const sendMailMock = jest.fn(); const mockLogger = loggingSystemMock.create().get() as jest.Mocked; -const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); -const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); -const connectorTokenClient = new ConnectorTokenClient({ - unsecuredSavedObjectsClient, - encryptedSavedObjectsClient, - logger: mockLogger, -}); +const connectorTokenClient = connectorTokenClientMock.create(); describe('send_email module', () => { beforeEach(() => { jest.resetAllMocks(); createTransportMock.mockReturnValue({ sendMail: sendMailMock }); sendMailMock.mockResolvedValue(sendMailMockResult); + + axios.create = jest.fn(() => { + const actual = jest.requireActual('axios'); + return { + ...actual.create, + interceptors: mockAxiosInstanceInterceptor, + }; + }); }); test('handles authenticated email using service', async () => { @@ -125,6 +132,7 @@ describe('send_email module', () => { delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; sendEmailGraphApiMock.mock.calls[0].pop(); + sendEmailGraphApiMock.mock.calls[0].pop(); expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { @@ -647,6 +655,83 @@ describe('send_email module', () => { ] `); }); + + test('deletes saved access tokens if 4xx response received', async () => { + const createAxiosInstanceMock = axios.create as jest.Mock; + const sendEmailOptions = getSendEmailOptions({ + transport: { + service: 'exchange_server', + clientId: '123456', + tenantId: '98765', + clientSecret: 'sdfhkdsjhfksdjfh', + }, + }); + (getOAuthClientCredentialsAccessToken as jest.Mock).mockResolvedValueOnce( + 'Bearer clienttokentokentoken' + ); + + await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); + expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1); + expect(createAxiosInstanceMock).toHaveBeenCalledWith(); + expect(mockAxiosInstanceInterceptor.response.use).toHaveBeenCalledTimes(1); + + const mockResponseCallback = (mockAxiosInstanceInterceptor.response.use as jest.Mock).mock + .calls[0][1]; + + const errorResponse = { + response: { + status: 403, + statusText: 'Forbidden', + data: { + error: { + message: 'Insufficient rights to query records', + detail: 'Field(s) present in the query do not have permission to be read', + }, + status: 'failure', + }, + }, + }; + + await expect(() => mockResponseCallback(errorResponse)).rejects.toEqual(errorResponse); + + expect(connectorTokenClient.deleteConnectorTokens).toHaveBeenCalledWith({ + connectorId: '1', + }); + }); + + test('does not delete saved access token if not 4xx error response received', async () => { + const createAxiosInstanceMock = axios.create as jest.Mock; + const sendEmailOptions = getSendEmailOptions({ + transport: { + service: 'exchange_server', + clientId: '123456', + tenantId: '98765', + clientSecret: 'sdfhkdsjhfksdjfh', + }, + }); + (getOAuthClientCredentialsAccessToken as jest.Mock).mockResolvedValueOnce( + 'Bearer clienttokentokentoken' + ); + + await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); + expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1); + expect(createAxiosInstanceMock).toHaveBeenCalledWith(); + expect(mockAxiosInstanceInterceptor.response.use).toHaveBeenCalledTimes(1); + + const mockResponseCallback = (mockAxiosInstanceInterceptor.response.use as jest.Mock).mock + .calls[0][1]; + + const errorResponse = { + response: { + status: 500, + statusText: 'Server error', + }, + }; + + await expect(() => mockResponseCallback(errorResponse)).rejects.toEqual(errorResponse); + + expect(connectorTokenClient.deleteConnectorTokens).not.toHaveBeenCalled(); + }); }); function getSendEmailOptions( diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index f2b059e51e0d6..2fee4dd8b377d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -5,6 +5,7 @@ * 2.0. */ +import axios, { AxiosResponse } from 'axios'; // info on nodemailer: https://nodemailer.com/about/ import nodemailer from 'nodemailer'; import { default as MarkdownIt } from 'markdown-it'; @@ -77,7 +78,7 @@ export async function sendEmail( } // send an email using MS Exchange Graph API -async function sendEmailWithExchange( +export async function sendEmailWithExchange( logger: Logger, options: SendEmailOptions, messageHTML: string, @@ -113,6 +114,30 @@ async function sendEmailWithExchange( Authorization: accessToken, }; + const axiosInstance = axios.create(); + axiosInstance.interceptors.response.use( + async (response: AxiosResponse) => { + // Look for 4xx errors that indicate something is wrong with the request + // We don't know for sure that it is an access token issue but remove saved + // token just to be sure + if (response.status >= 400 && response.status < 500) { + await connectorTokenClient.deleteConnectorTokens({ connectorId }); + } + return response; + }, + async (error) => { + const statusCode = error?.response?.status; + + // Look for 4xx errors that indicate something is wrong with the request + // We don't know for sure that it is an access token issue but remove saved + // token just to be sure + if (statusCode >= 400 && statusCode < 500) { + await connectorTokenClient.deleteConnectorTokens({ connectorId }); + } + return Promise.reject(error); + } + ); + return await sendEmailGraphApi( { options, @@ -121,7 +146,8 @@ async function sendEmailWithExchange( graphApiUrl: configurationUtilities.getMicrosoftGraphApiUrl(), }, logger, - configurationUtilities + configurationUtilities, + axiosInstance ); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts index c16cd884cb753..6475426143af7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts @@ -7,7 +7,7 @@ // @ts-expect-error missing type def import stringify from 'json-stringify-safe'; -import axios, { AxiosResponse } from 'axios'; +import axios, { AxiosInstance, AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; import { request } from './axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; @@ -25,11 +25,13 @@ const MICROSOFT_GRAPH_API_HOST = 'https://graph.microsoft.com/v1.0'; export async function sendEmailGraphApi( sendEmailOptions: SendEmailGraphApiOptions, logger: Logger, - configurationUtilities: ActionsConfigurationUtilities + configurationUtilities: ActionsConfigurationUtilities, + axiosInstance?: AxiosInstance ): Promise { const { options, headers, messageHTML, graphApiUrl } = sendEmailOptions; - const axiosInstance = axios.create(); + // Create a new axios instance if one is not provided + axiosInstance = axiosInstance ?? axios.create(); // POST /users/{id | userPrincipalName}/sendMail const res = await request({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts index 54cfa146c7e22..fcd2023dc8e27 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts @@ -192,17 +192,6 @@ describe('utils', () => { }); test('creates axios instance with interceptor when isOAuth is true and OAuth fields are defined', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: { - id: '1', - connectorId: '123', - tokenType: 'access_token', - token: 'testtokenvalue', - createdAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 10000000000).toISOString(), - }, - }); getAxiosInstance({ connectorId: '123', logger, @@ -260,5 +249,169 @@ describe('utils', () => { connectorTokenClient, }); }); + + test('throws expected error if getOAuthJwtAccessToken returns null access token', async () => { + getAxiosInstance({ + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + apiUrl: 'https://servicenow', + usesTableApi: true, + isOAuth: true, + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: null, + username: null, + password: null, + }, + }, + snServiceUrl: 'https://dev23432523.service-now.com', + connectorTokenClient, + }); + expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1); + expect(createAxiosInstanceMock).toHaveBeenCalledWith(); + expect(axiosInstanceMock.interceptors.request.use).toHaveBeenCalledTimes(1); + + (getOAuthJwtAccessToken as jest.Mock).mockResolvedValueOnce(null); + + const mockRequestCallback = (axiosInstanceMock.interceptors.request.use as jest.Mock).mock + .calls[0][0]; + + await expect(() => + mockRequestCallback({ headers: {} }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to retrieve access token for connectorId: 123"` + ); + + expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({ + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: null, + }, + }, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + connectorTokenClient, + }); + }); + + test('deletes saved access tokens if 4xx response received', async () => { + getAxiosInstance({ + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + apiUrl: 'https://servicenow', + usesTableApi: true, + isOAuth: true, + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: null, + username: null, + password: null, + }, + }, + snServiceUrl: 'https://dev23432523.service-now.com', + connectorTokenClient, + }); + expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1); + expect(createAxiosInstanceMock).toHaveBeenCalledWith(); + expect(axiosInstanceMock.interceptors.request.use).toHaveBeenCalledTimes(1); + expect(axiosInstanceMock.interceptors.response.use).toHaveBeenCalledTimes(1); + + (getOAuthJwtAccessToken as jest.Mock).mockResolvedValueOnce('Bearer tokentokentoken'); + + const mockResponseCallback = (axiosInstanceMock.interceptors.response.use as jest.Mock).mock + .calls[0][1]; + + const errorResponse = { + response: { + status: 403, + statusText: 'Forbidden', + data: { + error: { + message: 'Insufficient rights to query records', + detail: 'Field(s) present in the query do not have permission to be read', + }, + status: 'failure', + }, + }, + }; + + await expect(() => mockResponseCallback(errorResponse)).rejects.toEqual(errorResponse); + + expect(connectorTokenClient.deleteConnectorTokens).toHaveBeenCalledWith({ + connectorId: '123', + }); + }); + + test('does not delete saved access token if not 4xx error response received', async () => { + getAxiosInstance({ + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + apiUrl: 'https://servicenow', + usesTableApi: true, + isOAuth: true, + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: null, + username: null, + password: null, + }, + }, + snServiceUrl: 'https://dev23432523.service-now.com', + connectorTokenClient, + }); + expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1); + expect(createAxiosInstanceMock).toHaveBeenCalledWith(); + expect(axiosInstanceMock.interceptors.request.use).toHaveBeenCalledTimes(1); + expect(axiosInstanceMock.interceptors.response.use).toHaveBeenCalledTimes(1); + + (getOAuthJwtAccessToken as jest.Mock).mockResolvedValueOnce('Bearer tokentokentoken'); + + const mockResponseCallback = (axiosInstanceMock.interceptors.response.use as jest.Mock).mock + .calls[0][1]; + + const errorResponse = { + response: { + status: 500, + statusText: 'Server error', + }, + }; + + await expect(() => mockResponseCallback(errorResponse)).rejects.toEqual(errorResponse); + + expect(connectorTokenClient.deleteConnectorTokens).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts index f457220debb6d..92fd13d86e608 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; import { ExternalServiceCredentials, @@ -83,12 +83,12 @@ export const throwIfSubActionIsNotSupported = ({ }; export interface GetAxiosInstanceOpts { - connectorId?: string; + connectorId: string; logger: Logger; configurationUtilities: ActionsConfigurationUtilities; credentials: ExternalServiceCredentials; snServiceUrl: string; - connectorTokenClient?: ConnectorTokenClientContract; + connectorTokenClient: ConnectorTokenClientContract; } export const getAxiosInstance = ({ @@ -134,15 +134,28 @@ export const getAxiosInstance = ({ tokenUrl: `${snServiceUrl}/oauth_token.do`, connectorTokenClient, }); - - if (accessToken) { - axiosConfig.headers = { ...axiosConfig.headers, Authorization: accessToken }; + if (!accessToken) { + throw new Error(`Unable to retrieve access token for connectorId: ${connectorId}`); } - + axiosConfig.headers = { ...axiosConfig.headers, Authorization: accessToken }; return axiosConfig; }, (error) => { - Promise.reject(error); + return Promise.reject(error); + } + ); + axiosInstance.interceptors.response.use( + (response: AxiosResponse) => response, + async (error) => { + const statusCode = error?.response?.status; + + // Look for 4xx errors that indicate something is wrong with the request + // We don't know for sure that it is an access token issue but remove saved + // token just to be sure + if (statusCode >= 400 && statusCode < 500) { + await connectorTokenClient.deleteConnectorTokens({ connectorId }); + } + return Promise.reject(error); } ); } diff --git a/x-pack/plugins/aiops/README.md b/x-pack/plugins/aiops/README.md new file mode 100755 index 0000000000000..9bfd64f9bf3a3 --- /dev/null +++ b/x-pack/plugins/aiops/README.md @@ -0,0 +1,9 @@ +# aiops + +The plugin provides APIs and components for AIOps features, including the “Explain log rate spikes” UI, maintained by the ML team. + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/x-pack/plugins/aiops/common/api/example_stream.ts b/x-pack/plugins/aiops/common/api/example_stream.ts new file mode 100644 index 0000000000000..1210cccf55487 --- /dev/null +++ b/x-pack/plugins/aiops/common/api/example_stream.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const aiopsExampleStreamSchema = schema.object({ + /** Boolean flag to enable/disabling simulation of response errors. */ + simulateErrors: schema.maybe(schema.boolean()), + /** Maximum timeout between streaming messages. */ + timeout: schema.maybe(schema.number()), +}); + +export type AiopsExampleStreamSchema = TypeOf; + +export const API_ACTION_NAME = { + UPDATE_PROGRESS: 'update_progress', + ADD_TO_ENTITY: 'add_to_entity', + DELETE_ENTITY: 'delete_entity', +} as const; +export type ApiActionName = typeof API_ACTION_NAME[keyof typeof API_ACTION_NAME]; + +interface ApiActionUpdateProgress { + type: typeof API_ACTION_NAME.UPDATE_PROGRESS; + payload: number; +} + +export function updateProgressAction(payload: number): ApiActionUpdateProgress { + return { + type: API_ACTION_NAME.UPDATE_PROGRESS, + payload, + }; +} + +interface ApiActionAddToEntity { + type: typeof API_ACTION_NAME.ADD_TO_ENTITY; + payload: { + entity: string; + value: number; + }; +} + +export function addToEntityAction(entity: string, value: number): ApiActionAddToEntity { + return { + type: API_ACTION_NAME.ADD_TO_ENTITY, + payload: { + entity, + value, + }, + }; +} + +interface ApiActionDeleteEntity { + type: typeof API_ACTION_NAME.DELETE_ENTITY; + payload: string; +} + +export function deleteEntityAction(payload: string): ApiActionDeleteEntity { + return { + type: API_ACTION_NAME.DELETE_ENTITY, + payload, + }; +} + +export type ApiAction = ApiActionUpdateProgress | ApiActionAddToEntity | ApiActionDeleteEntity; diff --git a/x-pack/plugins/aiops/common/api/index.ts b/x-pack/plugins/aiops/common/api/index.ts new file mode 100644 index 0000000000000..da1e091d3fb54 --- /dev/null +++ b/x-pack/plugins/aiops/common/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AiopsExampleStreamSchema } from './example_stream'; + +export const API_ENDPOINT = { + EXAMPLE_STREAM: '/internal/aiops/example_stream', + ANOTHER: '/internal/aiops/another', +} as const; +export type ApiEndpoint = typeof API_ENDPOINT[keyof typeof API_ENDPOINT]; + +export interface ApiEndpointOptions { + [API_ENDPOINT.EXAMPLE_STREAM]: AiopsExampleStreamSchema; + [API_ENDPOINT.ANOTHER]: { anotherOption: string }; +} diff --git a/x-pack/plugins/aiops/common/index.ts b/x-pack/plugins/aiops/common/index.ts new file mode 100755 index 0000000000000..0f4835d67ecc7 --- /dev/null +++ b/x-pack/plugins/aiops/common/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * PLUGIN_ID is used as a unique identifier for the aiops plugin + */ +export const PLUGIN_ID = 'aiops'; + +/** + * PLUGIN_NAME is used as the display name for the aiops plugin + */ +export const PLUGIN_NAME = 'AIOps'; + +/** + * This is an internal hard coded feature flag so we can easily turn on/off the + * "Explain log rate spikes UI" during development until the first release. + */ +export const AIOPS_ENABLED = true; diff --git a/x-pack/plugins/aiops/jest.config.js b/x-pack/plugins/aiops/jest.config.js new file mode 100644 index 0000000000000..4b92cb8dc86cb --- /dev/null +++ b/x-pack/plugins/aiops/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/aiops'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/aiops', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/aiops/{common,public,server}/**/*.{js,ts,tsx}'], +}; diff --git a/x-pack/plugins/aiops/kibana.json b/x-pack/plugins/aiops/kibana.json new file mode 100755 index 0000000000000..b74a23bf2bc9e --- /dev/null +++ b/x-pack/plugins/aiops/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "aiops", + "version": "1.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Machine Learning UI", + "githubTeam": "ml-ui" + }, + "description": "AIOps plugin maintained by ML team.", + "server": true, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [], + "requiredBundles": ["kibanaReact"], + "extraPublicDirs": ["common"] +} diff --git a/x-pack/plugins/aiops/public/api/index.ts b/x-pack/plugins/aiops/public/api/index.ts new file mode 100644 index 0000000000000..6aa171df5286c --- /dev/null +++ b/x-pack/plugins/aiops/public/api/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 { lazyLoadModules } from '../lazy_load_bundle'; + +import type { ExplainLogRateSpikesSpec } from '../components/explain_log_rate_spikes'; + +export async function getExplainLogRateSpikesComponent(): Promise<() => ExplainLogRateSpikesSpec> { + const modules = await lazyLoadModules(); + return () => modules.ExplainLogRateSpikes; +} diff --git a/x-pack/plugins/aiops/public/components/app.tsx b/x-pack/plugins/aiops/public/components/app.tsx new file mode 100755 index 0000000000000..963253b154e27 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/app.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; + +import { Chart, Settings, Axis, BarSeries, Position, ScaleType } from '@elastic/charts'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +import { + EuiBadge, + EuiButton, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiProgress, + EuiSpacer, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { getStatusMessage } from './get_status_message'; +import { initialState, resetStream, streamReducer } from './stream_reducer'; +import { useStreamFetchReducer } from './use_stream_fetch_reducer'; + +export const AiopsApp = () => { + const { notifications } = useKibana(); + + const [simulateErrors, setSimulateErrors] = useState(false); + + const { dispatch, start, cancel, data, isCancelled, isRunning } = useStreamFetchReducer( + '/internal/aiops/example_stream', + streamReducer, + initialState, + { simulateErrors } + ); + + const { errors, progress, entities } = data; + + const onClickHandler = async () => { + if (isRunning) { + cancel(); + } else { + dispatch(resetStream()); + start(); + } + }; + + useEffect(() => { + if (errors.length > 0) { + notifications.toasts.danger({ body: errors[errors.length - 1] }); + } + }, [errors, notifications.toasts]); + + const buttonLabel = isRunning + ? i18n.translate('xpack.aiops.stopbuttonText', { + defaultMessage: 'Stop development', + }) + : i18n.translate('xpack.aiops.startbuttonText', { + defaultMessage: 'Start development', + }); + + return ( + + + + + +

    + +

    +
    +
    + + + + + + {buttonLabel} + + + + + {progress}% + + + + + + + +
    + + + + + + { + return { + x, + y, + }; + }) + .sort((a, b) => b.y - a.y)} + /> + +
    +

    {getStatusMessage(isRunning, isCancelled, data.progress)}

    + setSimulateErrors(!simulateErrors)} + compressed + /> +
    +
    +
    +
    +
    + ); +}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes.tsx new file mode 100644 index 0000000000000..21d7b39a2a148 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; + +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { I18nProvider } from '@kbn/i18n-react'; + +import { getCoreStart } from '../kibana_services'; + +import { AiopsApp } from './app'; + +/** + * Spec used for lazy loading in the ML plugin + */ +export type ExplainLogRateSpikesSpec = typeof ExplainLogRateSpikes; + +export const ExplainLogRateSpikes: FC = () => { + const coreStart = getCoreStart(); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/aiops/public/components/get_status_message.tsx b/x-pack/plugins/aiops/public/components/get_status_message.tsx new file mode 100644 index 0000000000000..e63748d03600a --- /dev/null +++ b/x-pack/plugins/aiops/public/components/get_status_message.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function getStatusMessage(isRunning: boolean, isCancelled: boolean, progress: number) { + if (!isRunning && !isCancelled && progress === 0) { + return 'Development did not start yet.'; + } else if (isRunning && !isCancelled) { + return 'Development is ongoing, the hype is real!'; + } else if (!isRunning && isCancelled) { + return 'Oh no, development got cancelled!'; + } else if (!isRunning && progress === 100) { + return 'Development clompeted, the release got out the door!'; + } + + // When the process stops but wasn't cancelled by the user and progress is not yet at 100%, + // this indicates there must have been a problem with the stream. + return 'Oh no, looks like there was a bug?!'; +} diff --git a/x-pack/plugins/aiops/public/components/stream_fetch.ts b/x-pack/plugins/aiops/public/components/stream_fetch.ts new file mode 100644 index 0000000000000..37d7c13dd3b55 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/stream_fetch.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type React from 'react'; + +import type { ApiEndpoint, ApiEndpointOptions } from '../../common/api'; + +export async function* streamFetch( + endpoint: E, + abortCtrl: React.MutableRefObject, + options: ApiEndpointOptions[ApiEndpoint], + basePath = '' +) { + const stream = await fetch(`${basePath}${endpoint}`, { + signal: abortCtrl.current.signal, + method: 'POST', + headers: { + // This refers to the format of the request body, + // not the response, which will be a uint8array Buffer. + 'Content-Type': 'application/json', + 'kbn-xsrf': 'stream', + }, + body: JSON.stringify(options), + }); + + if (stream.body !== null) { + // Note that Firefox 99 doesn't support `TextDecoderStream` yet. + // That's why we skip it here and use `TextDecoder` later to decode each chunk. + // Once Firefox supports it, we can use the following alternative: + // const reader = stream.body.pipeThrough(new TextDecoderStream()).getReader(); + const reader = stream.body.getReader(); + + const bufferBounce = 100; + let partial = ''; + let actionBuffer: A[] = []; + let lastCall = 0; + + while (true) { + try { + const { value: uint8array, done } = await reader.read(); + if (done) break; + + const value = new TextDecoder().decode(uint8array); + + const full = `${partial}${value}`; + const parts = full.split('\n'); + const last = parts.pop(); + + partial = last ?? ''; + + const actions = parts.map((p) => JSON.parse(p)); + actionBuffer.push(...actions); + + const now = Date.now(); + + if (now - lastCall >= bufferBounce && actionBuffer.length > 0) { + yield actionBuffer; + actionBuffer = []; + lastCall = now; + } + } catch (error) { + if (error.name !== 'AbortError') { + yield { type: 'error', payload: error.toString() }; + } + break; + } + } + + // The reader might finish with a partially filled actionBuffer so + // we need to clear it once more after the request is done. + if (actionBuffer.length > 0) { + yield actionBuffer; + actionBuffer.length = 0; + } + } +} diff --git a/x-pack/plugins/aiops/public/components/stream_reducer.ts b/x-pack/plugins/aiops/public/components/stream_reducer.ts new file mode 100644 index 0000000000000..3e68e139ceeca --- /dev/null +++ b/x-pack/plugins/aiops/public/components/stream_reducer.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApiAction, API_ACTION_NAME } from '../../common/api/example_stream'; + +export const UI_ACTION_NAME = { + ERROR: 'error', + RESET: 'reset', +} as const; +export type UiActionName = typeof UI_ACTION_NAME[keyof typeof UI_ACTION_NAME]; + +export interface StreamState { + errors: string[]; + progress: number; + entities: Record; +} +export const initialState: StreamState = { + errors: [], + progress: 0, + entities: {}, +}; + +interface UiActionError { + type: typeof UI_ACTION_NAME.ERROR; + payload: string; +} +interface UiActionResetStream { + type: typeof UI_ACTION_NAME.RESET; +} + +export function resetStream(): UiActionResetStream { + return { type: UI_ACTION_NAME.RESET }; +} + +type UiAction = UiActionResetStream | UiActionError; +export type ReducerAction = ApiAction | UiAction; +export function streamReducer( + state: StreamState, + action: ReducerAction | ReducerAction[] +): StreamState { + if (Array.isArray(action)) { + return action.reduce(streamReducer, state); + } + + switch (action.type) { + case API_ACTION_NAME.UPDATE_PROGRESS: + return { + ...state, + progress: action.payload, + }; + case API_ACTION_NAME.DELETE_ENTITY: + const deleteFromEntities = { ...state.entities }; + delete deleteFromEntities[action.payload]; + return { + ...state, + entities: deleteFromEntities, + }; + case API_ACTION_NAME.ADD_TO_ENTITY: + const addToEntities = { ...state.entities }; + if (addToEntities[action.payload.entity] === undefined) { + addToEntities[action.payload.entity] = action.payload.value; + } else { + addToEntities[action.payload.entity] += action.payload.value; + } + return { + ...state, + entities: addToEntities, + }; + case UI_ACTION_NAME.RESET: + return initialState; + case UI_ACTION_NAME.ERROR: + return { + ...state, + errors: [...state.errors, action.payload], + }; + default: + return { + ...state, + errors: [...state.errors, 'UNKNOWN_ACTION_ERROR'], + }; + } +} diff --git a/x-pack/plugins/aiops/public/components/use_stream_fetch_reducer.ts b/x-pack/plugins/aiops/public/components/use_stream_fetch_reducer.ts new file mode 100644 index 0000000000000..77ac09e0ff429 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/use_stream_fetch_reducer.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useReducer, useRef, useState, Reducer, ReducerAction, ReducerState } from 'react'; + +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +import type { ApiEndpoint, ApiEndpointOptions } from '../../common/api'; + +import { streamFetch } from './stream_fetch'; + +export const useStreamFetchReducer = , E = ApiEndpoint>( + endpoint: E, + reducer: R, + initialState: ReducerState, + options: ApiEndpointOptions[ApiEndpoint] +) => { + const kibana = useKibana(); + + const [isCancelled, setIsCancelled] = useState(false); + const [isRunning, setIsRunning] = useState(false); + + const [data, dispatch] = useReducer(reducer, initialState); + + const abortCtrl = useRef(new AbortController()); + + const start = async () => { + if (isRunning) { + throw new Error('Restart not supported yet'); + } + + setIsRunning(true); + setIsCancelled(false); + + abortCtrl.current = new AbortController(); + + for await (const actions of streamFetch( + endpoint, + abortCtrl, + options, + kibana.services.http?.basePath.get() + )) { + dispatch(actions as ReducerAction); + } + + setIsRunning(false); + }; + + const cancel = () => { + abortCtrl.current.abort(); + setIsCancelled(true); + setIsRunning(false); + }; + + return { + cancel, + data, + dispatch, + isCancelled, + isRunning, + start, + }; +}; diff --git a/x-pack/plugins/aiops/public/index.ts b/x-pack/plugins/aiops/public/index.ts new file mode 100755 index 0000000000000..30bcaf5afabdc --- /dev/null +++ b/x-pack/plugins/aiops/public/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AiopsPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new AiopsPlugin(); +} + +export type { AiopsPluginSetup, AiopsPluginStart } from './types'; + +export type { ExplainLogRateSpikesSpec } from './components/explain_log_rate_spikes'; diff --git a/x-pack/plugins/aiops/public/kibana_services.ts b/x-pack/plugins/aiops/public/kibana_services.ts new file mode 100644 index 0000000000000..9a43d2de5e5a1 --- /dev/null +++ b/x-pack/plugins/aiops/public/kibana_services.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from '@kbn/core/public'; +import { AppPluginStartDependencies } from './types'; + +let coreStart: CoreStart; +let pluginsStart: AppPluginStartDependencies; +export function setStartServices(core: CoreStart, plugins: AppPluginStartDependencies) { + coreStart = core; + pluginsStart = plugins; +} + +export const getCoreStart = () => coreStart; +export const getPluginsStart = () => pluginsStart; diff --git a/x-pack/plugins/aiops/public/lazy_load_bundle/index.ts b/x-pack/plugins/aiops/public/lazy_load_bundle/index.ts new file mode 100644 index 0000000000000..0072336080175 --- /dev/null +++ b/x-pack/plugins/aiops/public/lazy_load_bundle/index.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 type { ExplainLogRateSpikesSpec } from '../components/explain_log_rate_spikes'; + +let loadModulesPromise: Promise; + +interface LazyLoadedModules { + ExplainLogRateSpikes: ExplainLogRateSpikesSpec; +} + +export async function lazyLoadModules(): Promise { + if (typeof loadModulesPromise !== 'undefined') { + return loadModulesPromise; + } + + loadModulesPromise = new Promise(async (resolve, reject) => { + try { + const lazyImports = await import('./lazy'); + resolve({ ...lazyImports }); + } catch (error) { + reject(error); + } + }); + return loadModulesPromise; +} diff --git a/x-pack/plugins/aiops/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/aiops/public/lazy_load_bundle/lazy/index.ts new file mode 100644 index 0000000000000..967525de9bd6e --- /dev/null +++ b/x-pack/plugins/aiops/public/lazy_load_bundle/lazy/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { ExplainLogRateSpikesSpec } from '../../components/explain_log_rate_spikes'; +export { ExplainLogRateSpikes } from '../../components/explain_log_rate_spikes'; diff --git a/x-pack/plugins/aiops/public/plugin.ts b/x-pack/plugins/aiops/public/plugin.ts new file mode 100755 index 0000000000000..3c3cff39abb80 --- /dev/null +++ b/x-pack/plugins/aiops/public/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; + +import { getExplainLogRateSpikesComponent } from './api'; +import { setStartServices } from './kibana_services'; +import { AiopsPluginSetup, AiopsPluginStart } from './types'; + +export class AiopsPlugin implements Plugin { + public setup(core: CoreSetup) {} + + public start(core: CoreStart) { + setStartServices(core, {}); + return { + getExplainLogRateSpikesComponent, + }; + } + + public stop() {} +} diff --git a/x-pack/plugins/aiops/public/types.ts b/x-pack/plugins/aiops/public/types.ts new file mode 100755 index 0000000000000..fae18dc1d3106 --- /dev/null +++ b/x-pack/plugins/aiops/public/types.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 type { AiopsPlugin } from './plugin'; + +/** + * aiops plugin public setup contract + */ +export type AiopsPluginSetup = ReturnType; + +/** + * aiops plugin public start contract + */ +export type AiopsPluginStart = ReturnType; + +// eslint-disable-next-line +export type AppPluginStartDependencies = {}; diff --git a/x-pack/plugins/aiops/server/index.ts b/x-pack/plugins/aiops/server/index.ts new file mode 100755 index 0000000000000..8dca6eb397d5e --- /dev/null +++ b/x-pack/plugins/aiops/server/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 { PluginInitializerContext } from '@kbn/core/server'; +import { AiopsPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new AiopsPlugin(initializerContext); +} + +export type { AiopsPluginSetup, AiopsPluginStart } from './types'; diff --git a/x-pack/plugins/aiops/server/plugin.ts b/x-pack/plugins/aiops/server/plugin.ts new file mode 100755 index 0000000000000..c6b1b8b22a187 --- /dev/null +++ b/x-pack/plugins/aiops/server/plugin.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; + +import { AiopsPluginSetup, AiopsPluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class AiopsPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('aiops: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router, this.logger); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('aiops: Started'); + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/aiops/server/routes/index.ts b/x-pack/plugins/aiops/server/routes/index.ts new file mode 100755 index 0000000000000..e87c27e2af81e --- /dev/null +++ b/x-pack/plugins/aiops/server/routes/index.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { Readable } from 'stream'; + +import type { IRouter, Logger } from '@kbn/core/server'; + +import { AIOPS_ENABLED } from '../../common'; +import type { ApiAction } from '../../common/api/example_stream'; +import { + aiopsExampleStreamSchema, + updateProgressAction, + addToEntityAction, + deleteEntityAction, +} from '../../common/api/example_stream'; + +// We need this otherwise Kibana server will crash with a 'ERR_METHOD_NOT_IMPLEMENTED' error. +class ResponseStream extends Readable { + _read(): void {} +} + +const delimiter = '\n'; + +export function defineRoutes(router: IRouter, logger: Logger) { + if (AIOPS_ENABLED) { + router.post( + { + path: '/internal/aiops/example_stream', + validate: { + body: aiopsExampleStreamSchema, + }, + }, + async (context, request, response) => { + const maxTimeoutMs = request.body.timeout ?? 250; + const simulateError = request.body.simulateErrors ?? false; + + let shouldStop = false; + request.events.aborted$.subscribe(() => { + shouldStop = true; + }); + request.events.completed$.subscribe(() => { + shouldStop = true; + }); + + const stream = new ResponseStream(); + + function streamPush(d: ApiAction) { + try { + const line = JSON.stringify(d); + stream.push(`${line}${delimiter}`); + } catch (error) { + logger.error('Could not serialize or stream a message.'); + logger.error(error); + } + } + + const entities = [ + 'kimchy', + 's1monw', + 'martijnvg', + 'jasontedor', + 'nik9000', + 'javanna', + 'rjernst', + 'jrodewig', + ]; + + const actions = [...Array(19).fill('add'), 'delete']; + + if (simulateError) { + actions.push('server-only-error'); + actions.push('server-to-client-error'); + actions.push('client-error'); + } + + let progress = 0; + + async function pushStreamUpdate() { + setTimeout(() => { + try { + progress++; + + if (progress > 100 || shouldStop) { + stream.push(null); + return; + } + + streamPush(updateProgressAction(progress)); + + const randomEntity = entities[Math.floor(Math.random() * entities.length)]; + const randomAction = actions[Math.floor(Math.random() * actions.length)]; + + if (randomAction === 'add') { + const randomCommits = Math.floor(Math.random() * 100); + streamPush(addToEntityAction(randomEntity, randomCommits)); + } else if (randomAction === 'delete') { + streamPush(deleteEntityAction(randomEntity)); + } else if (randomAction === 'server-to-client-error') { + // Throw an error. It should not crash Kibana! + throw new Error('There was a (simulated) server side error!'); + } else if (randomAction === 'client-error') { + // Return not properly encoded JSON to the client. + stream.push(`{body:'Not valid JSON${delimiter}`); + } + + pushStreamUpdate(); + } catch (error) { + stream.push( + `${JSON.stringify({ type: 'error', payload: error.toString() })}${delimiter}` + ); + stream.push(null); + } + }, Math.floor(Math.random() * maxTimeoutMs)); + } + + // do not call this using `await` so it will run asynchronously while we return the stream already. + pushStreamUpdate(); + + return response.ok({ + body: stream, + }); + } + ); + } +} diff --git a/x-pack/plugins/aiops/server/types.ts b/x-pack/plugins/aiops/server/types.ts new file mode 100755 index 0000000000000..526e7280e9495 --- /dev/null +++ b/x-pack/plugins/aiops/server/types.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. + */ + +/** + * aiops plugin server setup contract + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AiopsPluginSetup {} + +/** + * aiops plugin server start contract + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AiopsPluginStart {} diff --git a/x-pack/plugins/aiops/tsconfig.json b/x-pack/plugins/aiops/tsconfig.json new file mode 100644 index 0000000000000..2545c0e21ed03 --- /dev/null +++ b/x-pack/plugins/aiops/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "scripts/**/*", + "server/**/*", + "types/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/custom_integrations/tsconfig.json" }, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/unified_search/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/apm/public/components/shared/stacktrace/cause_stacktrace.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/cause_stacktrace.tsx index afa9fd640472d..b132239902f9d 100644 --- a/x-pack/plugins/apm/public/components/shared/stacktrace/cause_stacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/stacktrace/cause_stacktrace.tsx @@ -18,7 +18,7 @@ const Accordion = euiStyled(EuiAccordion)` `; const CausedByContainer = euiStyled('h5')` - padding: ${({ theme }) => theme.eui.spacerSizes.s} 0; + padding: ${({ theme }) => theme.eui.euiSizeS} 0; `; const CausedByHeading = euiStyled('span')` diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts index 9e2367238da10..4c7b311d935d0 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts @@ -511,7 +511,7 @@ describe('getCommands', () => { expect(commands).not.toBe(''); expect(commands).toMatchInlineSnapshot(` "elastic_apm.server_url=\\"\\" - elastic.apm.secret_token=\\"\\" + elastic_apm.secret_token=\\"\\" elastic_apm.service_name=\\"My service\\" " `); @@ -527,7 +527,7 @@ describe('getCommands', () => { expect(commands).not.toBe(''); expect(commands).toMatchInlineSnapshot(` "elastic_apm.server_url=\\"localhost:8220\\" - elastic.apm.secret_token=\\"foobar\\" + elastic_apm.secret_token=\\"foobar\\" elastic_apm.service_name=\\"My service\\" " `); diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/php.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/php.ts index ea7e8764f89ad..dba4147b8afbc 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/commands/php.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/php.ts @@ -6,6 +6,6 @@ */ export const php = `elastic_apm.server_url="{{{apmServerUrl}}}" -elastic.apm.secret_token="{{{secretToken}}}" +elastic_apm.secret_token="{{{secretToken}}}" elastic_apm.service_name="My service" `; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot index beab512ea62e1..49e87d067e63a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__stories__/__snapshots__/extended_template.stories.storyshot @@ -75,7 +75,7 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = `
    `; @@ -155,7 +155,7 @@ exports[`Storyshots arguments/AxisConfig/components extended 1`] = `
    `; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot index 3cb7e726a9389..a868d023ec135 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__stories__/__snapshots__/date_format.stories.storyshot @@ -55,7 +55,7 @@ Array [
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,





    diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot index d7dc9a062e3ee..4b53b885aa7a8 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot @@ -88,7 +88,7 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = `
    can navigate Autoplay Settings 2`] = `




    export const CASE_ALERT_SUCCESS_TOAST = (title: string) => i18n.translate('xpack.cases.actions.caseAlertSuccessToast', { values: { title }, - defaultMessage: 'An alert has been added to "{title}"', + defaultMessage: 'An alert was added to "{title}"', }); export const CASE_ALERT_SUCCESS_SYNC_TEXT = i18n.translate( 'xpack.cases.actions.caseAlertSuccessSyncText', { - defaultMessage: 'Alerts in this case have their status synched with the case status', + defaultMessage: 'The alert statuses are synched with the case status.', } ); export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', { - defaultMessage: 'View Case', + defaultMessage: 'View case', }); export const APP_TITLE = i18n.translate('xpack.cases.common.appTitle', { diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index 4dfbe6495364d..e788f7b399bb4 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -83,7 +83,7 @@ describe('Use cases toast hook', () => { theCase: mockCase, attachments: [alertComment as SupportedCaseAttachment], }); - validateTitle('An alert has been added to "Another horrible breach!!'); + validateTitle('An alert was added to "Another horrible breach!!'); }); it('should display a generic title when called with a non-alert attachament', () => { @@ -130,7 +130,7 @@ describe('Use cases toast hook', () => { theCase: mockCase, attachments: [alertComment as SupportedCaseAttachment], }); - validateContent('Alerts in this case have their status synched with the case status'); + validateContent('The alert statuses are synched with the case status.'); }); it('renders empty content when called with an alert attachment and sync off', () => { @@ -144,7 +144,7 @@ describe('Use cases toast hook', () => { theCase: { ...mockCase, settings: { ...mockCase.settings, syncAlerts: false } }, attachments: [alertComment as SupportedCaseAttachment], }); - validateContent('View Case'); + validateContent('View case'); }); it('renders a correct successful message content', () => { @@ -152,7 +152,7 @@ describe('Use cases toast hook', () => { ); expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent('my content'); - expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); expect(onViewCaseClick).not.toHaveBeenCalled(); }); @@ -161,7 +161,7 @@ describe('Use cases toast hook', () => { ); expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); - expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); expect(onViewCaseClick).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 5d84454d038db..f0e951c89326f 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -26,7 +26,7 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & { const BottomContentWrapper = styled(EuiFlexGroup)` ${({ theme }) => ` - padding: ${theme.eui.ruleMargins.marginSmall} 0; + padding: ${theme.eui.euiSizeM} 0; `} `; diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_configuration.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_configuration.ts index f5d38e938e2cc..a796ace382d13 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_configuration.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_configuration.ts @@ -7,8 +7,10 @@ import { schema as rt, TypeOf } from '@kbn/config-schema'; export const cspRulesConfigSchema = rt.object({ - activated_rules: rt.object({ - cis_k8s: rt.arrayOf(rt.string()), + data_yaml: rt.object({ + activated_rules: rt.object({ + cis_k8s: rt.arrayOf(rt.string()), + }), }), }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts index 27dcd3cee6703..d0326fb037b60 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts @@ -13,7 +13,6 @@ import { httpServerMock, } from '@kbn/core/server/mocks'; import { - convertRulesConfigToYaml, createRulesConfig, defineUpdateRulesConfigRoute, getCspRules, @@ -144,7 +143,9 @@ describe('Update rules configuration API', () => { ], } as unknown as SavedObjectsFindResponse; const cspConfig = await createRulesConfig(cspRules); - expect(cspConfig).toMatchObject({ activated_rules: { cis_k8s: ['cis_1_1_1', 'cis_1_1_3'] } }); + expect(cspConfig).toMatchObject({ + data_yaml: { activated_rules: { cis_k8s: ['cis_1_1_1', 'cis_1_1_3'] } }, + }); }); it('create empty csp rules config when all rules are disabled', async () => { @@ -169,21 +170,13 @@ describe('Update rules configuration API', () => { ], } as unknown as SavedObjectsFindResponse; const cspConfig = await createRulesConfig(cspRules); - expect(cspConfig).toMatchObject({ activated_rules: { cis_k8s: [] } }); - }); - - it('validate converting rules config object to Yaml', async () => { - const cspRuleConfig = { activated_rules: { cis_k8s: ['1.1.1', '1.1.2'] } }; - - const dataYaml = convertRulesConfigToYaml(cspRuleConfig); - - expect(dataYaml).toEqual('activated_rules:\n cis_k8s:\n - 1.1.1\n - 1.1.2\n'); + expect(cspConfig).toMatchObject({ data_yaml: { activated_rules: { cis_k8s: [] } } }); }); it('validate adding new data.yaml to package policy instance', async () => { const packagePolicy = createPackagePolicyMock(); - const dataYaml = 'activated_rules:\n cis_k8s:\n - 1.1.1\n - 1.1.2\n'; + const dataYaml = 'data_yaml:\n activated_rules:\n cis_k8s:\n - 1.1.1\n - 1.1.2\n'; const updatedPackagePolicy = setVarToPackagePolicy(packagePolicy, dataYaml); expect(updatedPackagePolicy.vars).toEqual({ dataYaml: { type: 'config', value: dataYaml } }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts index 21587394d51e8..72c19fd5e37dd 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts @@ -66,8 +66,10 @@ export const createRulesConfig = ( ): CspRulesConfigSchema => { const activatedRules = cspRules.saved_objects.filter((cspRule) => cspRule.attributes.enabled); const config = { - activated_rules: { - cis_k8s: activatedRules.map((activatedRule) => activatedRule.attributes.rego_rule_id), + data_yaml: { + activated_rules: { + cis_k8s: activatedRules.map((activatedRule) => activatedRule.attributes.rego_rule_id), + }, }, }; return config; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index 13a13c25a5ad8..79460ac486f1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -13,6 +13,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { docLinks } from '../../../shared/doc_links'; import { WEB_CRAWLER_DOCS_URL, WEB_CRAWLER_LOG_DOCS_URL } from '../../routes'; import { getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; @@ -43,6 +46,34 @@ export const CrawlerOverview: React.FC = () => { pageHeader={{ pageTitle: CRAWLER_TITLE, rightSideItems: [, ], + description: ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.ingestionPluginDocumentationLink', + { defaultMessage: 'Elasticsearch ingest attachment plugin' } + )} + + ), + deploymentSettingsDocumentationLink: ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deploymentSettingsDocumentationLink', + { defaultMessage: 'review your deployment settings' } + )} + + ), + }} + /> + ), }} isLoading={dataLoading} > diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx index 3bc7eac1a9e69..a5d0e8facf236 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx @@ -44,7 +44,7 @@ describe('DocumentCreationFlyout', () => { const wrapper = shallow(); expect(wrapper.find(EuiFlyout)).toHaveLength(1); - wrapper.find(EuiFlyout).prop('onClose')(); + wrapper.find(EuiFlyout).prop('onClose')(new MouseEvent('click')); expect(actions.closeDocumentCreation).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index b507e5466f13f..b037a5aed6217 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -30,6 +30,7 @@ class DocLinks { public appSearchSynonyms: string; public appSearchWebCrawler: string; public appSearchWebCrawlerEventLogs: string; + public appSearchWebCrawlerReference: string; public clientsGoIndex: string; public clientsGuide: string; public clientsJavaBasicAuthentication: string; @@ -58,6 +59,7 @@ class DocLinks { public enterpriseSearchUsersAccess: string; public kibanaSecurity: string; public licenseManagement: string; + public pluginsIngestAttachment: string; public queryDsl: string; public workplaceSearchApiKeys: string; public workplaceSearchBox: string; @@ -110,6 +112,7 @@ class DocLinks { this.appSearchSynonyms = ''; this.appSearchWebCrawler = ''; this.appSearchWebCrawlerEventLogs = ''; + this.appSearchWebCrawlerReference = ''; this.clientsGoIndex = ''; this.clientsGuide = ''; this.clientsJavaBasicAuthentication = ''; @@ -138,6 +141,7 @@ class DocLinks { this.enterpriseSearchUsersAccess = ''; this.kibanaSecurity = ''; this.licenseManagement = ''; + this.pluginsIngestAttachment = ''; this.queryDsl = ''; this.workplaceSearchApiKeys = ''; this.workplaceSearchBox = ''; @@ -192,6 +196,7 @@ class DocLinks { this.appSearchSynonyms = docLinks.links.appSearch.synonyms; this.appSearchWebCrawler = docLinks.links.appSearch.webCrawler; this.appSearchWebCrawlerEventLogs = docLinks.links.appSearch.webCrawlerEventLogs; + this.appSearchWebCrawlerReference = docLinks.links.appSearch.webCrawlerReference; this.clientsGoIndex = docLinks.links.clients.goIndex; this.clientsGuide = docLinks.links.clients.guide; this.clientsJavaBasicAuthentication = docLinks.links.clients.javaBasicAuthentication; @@ -220,6 +225,7 @@ class DocLinks { this.enterpriseSearchUsersAccess = docLinks.links.enterpriseSearch.usersAccess; this.kibanaSecurity = docLinks.links.kibana.xpackSecurity; this.licenseManagement = docLinks.links.enterpriseSearch.licenseManagement; + this.pluginsIngestAttachment = docLinks.links.plugins.ingestAttachment; this.queryDsl = docLinks.links.query.queryDsl; this.workplaceSearchApiKeys = docLinks.links.workplaceSearch.apiKeys; this.workplaceSearchBox = docLinks.links.workplaceSearch.box; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx index fad3596bc8122..07ae7e0e46435 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx @@ -33,7 +33,7 @@ const FlyoutWithHigherZIndex = styled(EuiFlyout)` z-index: ${(props) => props.theme.eui.euiZLevel5}; `; -interface Props extends EuiFlyoutProps { +interface Props extends Omit { onClose: (createdAgentPolicy?: AgentPolicy) => void; } diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/icon_panel.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/icon_panel.tsx index cc8b61e103be4..cb388ba4b7443 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/icon_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/icon_panel.tsx @@ -14,7 +14,7 @@ import { usePackageIconType } from '../../../../../hooks'; import { Loading } from '../../../../../components'; const Panel = styled(EuiPanel)` - padding: ${(props) => props.theme.eui.spacerSizes.xl}; + padding: ${(props) => props.theme.eui.euiSizeXL}; width: ${(props) => parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.euiSizeXL) * 2}px; svg, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 9202d89d7c93b..05ff443a7b0e6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -48,8 +48,8 @@ import { UpdateButton } from './update_button'; import { UninstallButton } from './uninstall_button'; const SettingsTitleCell = styled.td` - padding-right: ${(props) => props.theme.eui.spacerSizes.xl}; - padding-bottom: ${(props) => props.theme.eui.spacerSizes.m}; + padding-right: ${(props) => props.theme.eui.euiSizeXL}; + padding-bottom: ${(props) => props.theme.eui.euiSizeM}; `; const NoteLabel = () => ( diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap index 802d684a8a261..6b54e1d3f43f5 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap @@ -95,779 +95,258 @@ Array [ `; exports[`extend index management ilm summary extension should return extension when index has lifecycle error 1`] = ` - - -

    - - Index lifecycle management - -

    -
    - , +
    , +
    - - +
    - - - - Index lifecycle error - - + illegal_argument_exception: setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined
    - +
    , +
    , +
    +
    +
    - - + testy + + +
    + + Current action + +
    +
    + rollover +
    +
    + + Failed step + +
    +
    + check-rollover-ready +
    +
    - -
    - - -
    - -
    +
    - -
    - -
    - - Lifecycle policy - -
    -
    - -
    - - - testy - - -
    -
    - -
    - - Current action - -
    -
    - -
    - rollover -
    -
    - -
    - - Failed step - -
    -
    - -
    - check-rollover-ready -
    -
    -
    -
    -
    -
    - -
    + Current phase + + +
    + hot +
    +
    + + Current action time + +
    +
    + 2018-12-07 13:02:55 +
    +
    + + Phase definition + +
    +
    - -
    - -
    - - Current phase - -
    -
    - -
    - hot -
    -
    - -
    - - Current action time - -
    -
    - -
    - 2018-12-07 13:02:55 -
    -
    - -
    - - - Phase definition - - -
    -
    - -
    - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="phaseExecutionPopover" - isOpen={false} - key="phaseExecutionPopover" - ownFocus={true} - panelPaddingSize="m" - > -
    -
    - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    + Show definition + +
    +
    + +
    - - +
    , +] `; exports[`extend index management ilm summary extension should return extension when index has lifecycle policy 1`] = ` - - -

    - - Index lifecycle management - -

    -
    - , +
    , +
    - - -
    - -
    +
    - -
    - -
    - - Lifecycle policy - -
    -
    - -
    - - - testy - - -
    -
    - -
    - - Current action - -
    -
    - -
    - complete -
    -
    - -
    - - Failed step - -
    -
    - -
    - - -
    -
    -
    -
    -
    -
    - -
    + Lifecycle policy + + +
    - -
    - -
    - - Current phase - -
    -
    - -
    - new -
    -
    - -
    - - Current action time - -
    -
    - -
    - 2018-12-07 13:02:55 -
    -
    -
    -
    -
    -
    + testy + + +
    + + Current action + +
    +
    + complete +
    +
    + + Failed step + +
    +
    + - +
    +
    -
    - +
    +
    +
    + + Current phase + +
    +
    + new +
    +
    + + Current action time + +
    +
    + 2018-12-07 13:02:55 +
    +
    +
    +
    , +] `; exports[`extend index management remove lifecycle action extension should return extension when all indices have lifecycle policy 1`] = ` diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap index f4d7fc149a694..8cbb4aa450c7c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap @@ -70,7 +70,7 @@ exports[`policy table shows empty state when there are no policies 1`] = ` class="euiTextColor euiTextColor--subdued" >
    -
    -
    -
    - Confirm License Upload -
    -
    -
    -
    -
    -
    -
    - Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
    -
    -
      -
    • - Watcher will be disabled -
    • -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - } - > - -
    -
    - - - - - -
    - -
    - -
    - - Confirm License Upload - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    - Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
    -
    - -
    -
      -
    • - Watcher will be disabled -
    • -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - - - - - - - - -
    -
    -
    -
    -
    -
    - - - - - -
    -

    - - Your license key is a JSON file with a signature attached. - -

    -

    - - - , - } - } - > - Uploading a license will replace your current - - license. - -

    -
    - - -
    - - -
    - - } - onChange={[Function]} - > - -
    -
    - - - -
    - -
    - - Select or drag your license file - -
    -
    -
    -
    -
    -
    - -
    - - -
    - - -
    - - - - -
    - - - - - -
    -
    -
    -
    -
    - -
    - + Select or drag your license file
    - - - - - - +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    `; exports[`UploadLicense should display an error when ES says license is expired 1`] = ` - - - - +
    +
    +
    +

    + Your license key is a JSON file with a signature attached. +

    +

    + Uploading a license will replace your current + + license. +

    +
    +
    +
    - - - + Please address the highlighted errors. + +
    +
    +
    +
      +
    • + The supplied license is not valid for this product. +
    • +
    +
    +
    +
    +
    +
    + +
    + `; exports[`UploadLicense should display an error when submitting invalid JSON 1`] = ` - - - - +
    +
    +

    + Your license key is a JSON file with a signature attached. +

    +

    + Uploading a license will replace your current + + license. +

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

    - - Upload your license - -

    -
    - -
    - - -
    -

    - - Your license key is a JSON file with a signature attached. - -

    -

    - - - , - } - } - > - Uploading a license will replace your current - - license. - -

    -
    - - -
    - - -
    - - -
    -
    - - Please address the highlighted errors. - -
    - -
    - -
    -
      -
    • - Error encountered uploading license: Check your license file. -
    • -
    -
    -
    -
    -
    -
    -
    -
    - - } - onChange={[Function]} - > - -
    -
    - - - -
    - -
    - - Select or drag your license file - -
    -
    -
    -
    -
    -
    - -
    - - -
    - - -
    - - - - -
    - - - - - -
    -
    -
    -
    -
    - -
    - + Select or drag your license file
    - - - - - - +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    `; exports[`UploadLicense should display error when ES returns error 1`] = ` - - - + Upload your license +
    +
    +
    +

    + Your license key is a JSON file with a signature attached. +

    +

    + Uploading a license will replace your current + + license. +

    +
    +
    +
    - +
    + + Please address the highlighted errors. + +
    +
    +
    +
      +
    • + Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled +
    • +
    +
    +
    +
    +
    - - +
    +
    - -
    - -

    - - Upload your license - -

    -
    - -
    - - -
    -

    - - Your license key is a JSON file with a signature attached. - -

    -

    - - - , - } - } - > - Uploading a license will replace your current - - license. - -

    -
    - - -
    - - -
    - - -
    -
    - - Please address the highlighted errors. - -
    - -
    - -
    -
      -
    • - Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled -
    • -
    -
    -
    -
    -
    -
    -
    -
    - - } - onChange={[Function]} - > - -
    -
    - - - -
    - -
    - - Select or drag your license file - -
    -
    -
    -
    -
    -
    - -
    - - -
    - - -
    - - - - -
    - - - - - -
    -
    -
    -
    -
    - -
    - + Select or drag your license file
    - - - - - - +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    `; diff --git a/x-pack/plugins/license_management/__jest__/upload_license.test.tsx b/x-pack/plugins/license_management/__jest__/upload_license.test.tsx index eb38aab4470d8..c24c2bf6a9c6b 100644 --- a/x-pack/plugins/license_management/__jest__/upload_license.test.tsx +++ b/x-pack/plugins/license_management/__jest__/upload_license.test.tsx @@ -89,7 +89,7 @@ describe('UploadLicense', () => { const rendered = mountWithIntl(component); store.dispatch(uploadLicense('INVALID', 'trial')); rendered.update(); - expect(rendered).toMatchSnapshot(); + expect(rendered.render()).toMatchSnapshot(); }); it('should display an error when ES says license is invalid', async () => { @@ -98,7 +98,7 @@ describe('UploadLicense', () => { const invalidLicense = JSON.stringify({ license: { type: 'basic' } }); await uploadLicense(invalidLicense)(store.dispatch, null, thunkServices); rendered.update(); - expect(rendered).toMatchSnapshot(); + expect(rendered.render()).toMatchSnapshot(); }); it('should display an error when ES says license is expired', async () => { @@ -107,7 +107,7 @@ describe('UploadLicense', () => { const invalidLicense = JSON.stringify({ license: { type: 'basic' } }); await uploadLicense(invalidLicense)(store.dispatch, null, thunkServices); rendered.update(); - expect(rendered).toMatchSnapshot(); + expect(rendered.render()).toMatchSnapshot(); }); it('should display a modal when license requires acknowledgement', async () => { @@ -117,7 +117,7 @@ describe('UploadLicense', () => { }); await uploadLicense(unacknowledgedLicense, 'trial')(store.dispatch, null, thunkServices); const rendered = mountWithIntl(component); - expect(rendered).toMatchSnapshot(); + expect(rendered.render()).toMatchSnapshot(); }); it('should refresh xpack info and navigate to BASE_PATH when ES accepts new license', async () => { @@ -134,6 +134,6 @@ describe('UploadLicense', () => { const license = JSON.stringify({ license: { type: 'basic' } }); await uploadLicense(license)(store.dispatch, null, thunkServices); rendered.update(); - expect(rendered).toMatchSnapshot(); + expect(rendered.render()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index edbf4df979f7b..5945ee3d35d8b 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -25,8 +25,7 @@ "mapsEms", "savedObjects", "share", - "presentationUtil", - "screenshotMode" + "presentationUtil" ], "optionalPlugins": [ "cloud", @@ -34,6 +33,7 @@ "home", "savedObjectsTagging", "charts", + "screenshotMode", "security", "spaces", "usageCollection" diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/pluck_style_meta_from_features.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/pluck_style_meta_from_features.test.ts index 5c609f66e3f53..31aea302b70b1 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/pluck_style_meta_from_features.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/pluck_style_meta_from_features.test.ts @@ -188,7 +188,7 @@ describe('pluckStyleMetaFromFeatures', () => { }); }); - test('Should extract scaled field range', async () => { + test('Should extract range', async () => { const features = [ { type: 'Feature', @@ -197,7 +197,7 @@ describe('pluckStyleMetaFromFeatures', () => { coordinates: [0, 0], }, properties: { - myDynamicField: 1, + myDynamicField: 3, }, }, { @@ -242,9 +242,9 @@ describe('pluckStyleMetaFromFeatures', () => { myDynamicField: { categories: [], range: { - delta: 9, + delta: 7, max: 10, - min: 1, + min: 3, }, }, }, @@ -255,6 +255,65 @@ describe('pluckStyleMetaFromFeatures', () => { }, }); }); + + test('Should extract range with "min = 1" for count field', async () => { + const features = [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [0, 0], + }, + properties: { + count: 3, + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [0, 0], + }, + properties: { + count: 10, + }, + }, + ] as Feature[]; + const dynamicColorOptions = { + type: COLOR_MAP_TYPE.ORDINAL, + field: { + origin: FIELD_ORIGIN.SOURCE, + name: 'count', + }, + } as ColorDynamicOptions; + const field = new InlineField({ + fieldName: dynamicColorOptions.field!.name, + source: {} as unknown as IVectorSource, + origin: dynamicColorOptions.field!.origin, + dataType: 'number', + }); + field.isCount = () => { + return true; + }; + const dynamicColorProperty = new DynamicColorProperty( + dynamicColorOptions, + VECTOR_STYLES.FILL_COLOR, + field, + {} as unknown as IVectorLayer, + () => { + return null; + } // getFieldFormatter + ); + + const styleMeta = await pluckStyleMetaFromFeatures(features, Object.values(VECTOR_SHAPE_TYPE), [ + dynamicColorProperty, + ]); + expect(styleMeta.fieldMeta.count.range).toEqual({ + delta: 9, + max: 10, + min: 1, + }); + }); }); describe('pluckCategoricalStyleMetaFromFeatures', () => { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/pluck_style_meta_from_features.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/pluck_style_meta_from_features.ts index 2ea0fef1bf648..7867161a14e21 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/pluck_style_meta_from_features.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/pluck_style_meta_from_features.ts @@ -113,18 +113,22 @@ function pluckOrdinalStyleMetaFromFeatures( property: IDynamicStyleProperty, features: Feature[] ): RangeFieldMeta | null { - if (!property.isOrdinal()) { + const field = property.getField(); + if (!field || !property.isOrdinal()) { return null; } const name = property.getFieldName(); - let min = Infinity; + const isCount = field.isCount(); + let min = isCount ? 1 : Infinity; let max = -Infinity; for (let i = 0; i < features.length; i++) { const feature = features[i]; const newValue = feature.properties ? parseFloat(feature.properties[name]) : NaN; if (!isNaN(newValue)) { - min = Math.min(min, newValue); + if (!isCount) { + min = Math.min(min, newValue); + } max = Math.max(max, newValue); } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.test.ts new file mode 100644 index 0000000000000..c0e624d412bc3 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.test.ts @@ -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 { FIELD_ORIGIN } from '../../../../../common/constants'; +import { TileMetaFeature } from '../../../../../common/descriptor_types'; +import { pluckOrdinalStyleMeta } from './pluck_style_meta'; +import { IField } from '../../../fields/field'; +import { DynamicSizeProperty } from '../../../styles/vector/properties/dynamic_size_property'; + +describe('pluckOrdinalStyleMeta', () => { + test('should pluck range from metaFeatures', () => { + const mockField = { + isCount: () => { + return false; + }, + pluckRangeFromTileMetaFeature: (metaFeature: TileMetaFeature) => { + return { + max: metaFeature.properties['aggregations.avg_of_bytes.max'], + min: metaFeature.properties['aggregations.avg_of_bytes.min'], + }; + }, + } as unknown as IField; + const mockStyleProperty = { + getField: () => { + return mockField; + }, + isOrdinal: () => { + return true; + }, + getFieldOrigin: () => { + return FIELD_ORIGIN.SOURCE; + }, + } as unknown as DynamicSizeProperty; + const metaFeatures = [ + { + properties: { + 'aggregations.avg_of_bytes.max': 7565, + 'aggregations.avg_of_bytes.min': 1622, + }, + } as unknown as TileMetaFeature, + { + properties: { + 'aggregations.avg_of_bytes.max': 11869, + 'aggregations.avg_of_bytes.min': 659, + }, + } as unknown as TileMetaFeature, + ]; + expect(pluckOrdinalStyleMeta(mockStyleProperty, metaFeatures, undefined)).toEqual({ + max: 11869, + min: 659, + delta: 11210, + }); + }); + + test('should pluck range with min: 1 from metaFeatures for count field', () => { + const mockField = { + isCount: () => { + return true; + }, + pluckRangeFromTileMetaFeature: (metaFeature: TileMetaFeature) => { + return { + max: metaFeature.properties['aggregations._count.max'], + min: metaFeature.properties['aggregations._count.min'], + }; + }, + } as unknown as IField; + const mockStyleProperty = { + getField: () => { + return mockField; + }, + isOrdinal: () => { + return true; + }, + getFieldOrigin: () => { + return FIELD_ORIGIN.SOURCE; + }, + } as unknown as DynamicSizeProperty; + const metaFeatures = [ + { + properties: { + 'aggregations._count.max': 35, + 'aggregations._count.min': 3, + }, + } as unknown as TileMetaFeature, + { + properties: { + 'aggregations._count.max': 36, + 'aggregations._count.min': 5, + }, + } as unknown as TileMetaFeature, + ]; + expect(pluckOrdinalStyleMeta(mockStyleProperty, metaFeatures, undefined)).toEqual({ + max: 36, + min: 1, + delta: 35, + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.ts similarity index 92% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.tsx rename to x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.ts index 1f9784fb65dc0..564500b59742b 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/pluck_style_meta.ts @@ -70,7 +70,7 @@ function pluckCategoricalStyleMeta( return []; } -function pluckOrdinalStyleMeta( +export function pluckOrdinalStyleMeta( property: IDynamicStyleProperty, metaFeatures: TileMetaFeature[], joinPropertiesMap: PropertiesMap | undefined @@ -80,13 +80,16 @@ function pluckOrdinalStyleMeta( return null; } - let min = Infinity; + const isCount = field.isCount(); + let min = isCount ? 1 : Infinity; let max = -Infinity; if (property.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { for (let i = 0; i < metaFeatures.length; i++) { const range = field.pluckRangeFromTileMetaFeature(metaFeatures[i]); if (range) { - min = Math.min(range.min, min); + if (!isCount) { + min = Math.min(range.min, min); + } max = Math.max(range.max, max); } } @@ -94,7 +97,9 @@ function pluckOrdinalStyleMeta( joinPropertiesMap.forEach((value: { [key: string]: unknown }) => { const propertyValue = value[field.getName()]; if (typeof propertyValue === 'number') { - min = Math.min(propertyValue as number, min); + if (!isCount) { + min = Math.min(propertyValue as number, min); + } max = Math.max(propertyValue as number, max); } }); diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 969a985601452..131883eff40ce 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -21,7 +21,7 @@ import { Timeslider } from '../timeslider'; import { ToolbarOverlay } from '../toolbar_overlay'; import { EditLayerPanel } from '../edit_layer_panel'; import { AddLayerPanel } from '../add_layer_panel'; -import { getData } from '../../kibana_services'; +import { getData, isScreenshotMode } from '../../kibana_services'; import { RawValue } from '../../../common/constants'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapSettings } from '../../reducers/map'; @@ -231,7 +231,7 @@ export class MapContainer extends Component { onSingleValueTrigger={onSingleValueTrigger} renderTooltipContent={renderTooltipContent} /> - {!this.props.settings.hideToolbarOverlay && ( + {!this.props.settings.hideToolbarOverlay && !isScreenshotMode() && ( { } if ( - this._prevDisableInteractive === undefined || - this._prevDisableInteractive !== this.props.settings.disableInteractive + !isScreenshotMode() && + (this._prevDisableInteractive === undefined || + this._prevDisableInteractive !== this.props.settings.disableInteractive) ) { this._prevDisableInteractive = this.props.settings.disableInteractive; if (this.props.settings.disableInteractive) { diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/__snapshots__/attribution_control.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/__snapshots__/attribution_control.test.tsx.snap index 324a8f1e7fc45..014ee4d5f0f2a 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/__snapshots__/attribution_control.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/__snapshots__/attribution_control.test.tsx.snap @@ -8,17 +8,15 @@ exports[`AttributionControl is rendered 1`] = ` size="xs" > - - - attribution with link - - , - attribution with no link - + + attribution with link + + , + attribution with no link
    diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/attribution_control.test.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/attribution_control.test.tsx index 630e06f014bc6..85b4208ae2251 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/attribution_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/attribution_control.test.tsx @@ -5,6 +5,12 @@ * 2.0. */ +jest.mock('../../../kibana_services', () => ({ + isScreenshotMode: () => { + return false; + }, +})); + import React from 'react'; import { shallow } from 'enzyme'; import { ILayer } from '../../../classes/layers/layer'; diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/attribution_control.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/attribution_control.tsx index 4b42bc482a702..098f603a99061 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/attribution_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/attribution_control/attribution_control.tsx @@ -11,6 +11,7 @@ import { EuiText, EuiLink } from '@elastic/eui'; import classNames from 'classnames'; import { Attribution } from '../../../../common/descriptor_types'; import { ILayer } from '../../../classes/layers/layer'; +import { isScreenshotMode } from '../../../kibana_services'; export interface Props { isFullScreen: boolean; @@ -74,11 +75,9 @@ export class AttributionControl extends Component { }; _renderAttribution({ url, label }: Attribution) { - if (!url) { - return label; - } - - return ( + return !url || isScreenshotMode() ? ( + label + ) : ( {label} @@ -108,9 +107,7 @@ export class AttributionControl extends Component { })} > - - {this._renderAttributions()} - + {this._renderAttributions()}
    ); diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_control.test.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_control.test.tsx index 0526eddc6521d..649999ab49a9d 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_control.test.tsx @@ -11,6 +11,12 @@ jest.mock('./layer_toc', () => ({ }, })); +jest.mock('../../../kibana_services', () => ({ + isScreenshotMode: () => { + return false; + }, +})); + import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_control.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_control.tsx index 0e692cb130237..d131bf9b98026 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_control.tsx @@ -20,6 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { LayerTOC } from './layer_toc'; +import { isScreenshotMode } from '../../../kibana_services'; import { ILayer } from '../../../classes/layers/layer'; export interface Props { @@ -82,6 +83,9 @@ export function LayerControl({ isFlyoutOpen, }: Props) { if (!isLayerTOCOpen) { + if (isScreenshotMode()) { + return null; + } const hasErrors = layerList.some((layer) => { return layer.hasErrors(); }); diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index ba536953bb8f0..5774a46684644 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -67,6 +67,9 @@ export const getSpacesApi = () => pluginsStart.spaces; export const getTheme = () => coreStart.theme; export const getUsageCollection = () => pluginsStart.usageCollection; export const getApplication = () => coreStart.application; +export const isScreenshotMode = () => { + return pluginsStart.screenshotMode ? pluginsStart.screenshotMode.isScreenshotMode() : false; +}; // xpack.maps.* kibana.yml settings from this plugin let mapAppConfig: MapsConfigType; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index ed53f3a05aff0..846e7fc3d83f7 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -89,7 +89,7 @@ export interface MapsPluginSetupDependencies { share: SharePluginSetup; licensing: LicensingPluginSetup; usageCollection?: UsageCollectionSetup; - screenshotMode: ScreenshotModePluginSetup; + screenshotMode?: ScreenshotModePluginSetup; } export interface MapsPluginStartDependencies { @@ -112,6 +112,7 @@ export interface MapsPluginStartDependencies { security?: SecurityPluginStart; spaces?: SpacesPluginStart; mapsEms: MapsEmsPluginPublicStart; + screenshotMode?: ScreenshotModePluginSetup; usageCollection?: UsageCollectionSetup; } @@ -151,7 +152,7 @@ export class MapsPlugin // Override this when we know we are taking a screenshot (i.e. no user interaction) // to avoid a blank-canvas issue when rendering maps on a PDF - preserveDrawingBuffer: plugins.screenshotMode.isScreenshotMode() + preserveDrawingBuffer: plugins.screenshotMode?.isScreenshotMode() ? true : config.preserveDrawingBuffer, }); diff --git a/x-pack/plugins/ml/common/constants/locator.ts b/x-pack/plugins/ml/common/constants/locator.ts index 0a1c2638e684a..0c19c5b59766c 100644 --- a/x-pack/plugins/ml/common/constants/locator.ts +++ b/x-pack/plugins/ml/common/constants/locator.ts @@ -51,6 +51,8 @@ export const ML_PAGES = { FILTER_LISTS_EDIT: 'settings/filter_lists/edit_filter_list', ACCESS_DENIED: 'access-denied', OVERVIEW: 'overview', + AIOPS: 'aiops', + AIOPS_EXPLAIN_LOG_RATE_SPIKES: 'aiops/explain_log_rate_spikes', } as const; export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES]; diff --git a/x-pack/plugins/ml/common/types/locator.ts b/x-pack/plugins/ml/common/types/locator.ts index 33ec94b825303..a440aaa349bcc 100644 --- a/x-pack/plugins/ml/common/types/locator.ts +++ b/x-pack/plugins/ml/common/types/locator.ts @@ -60,7 +60,9 @@ export type MlGenericUrlState = MLPageState< | typeof ML_PAGES.ACCESS_DENIED | typeof ML_PAGES.DATA_VISUALIZER | typeof ML_PAGES.DATA_VISUALIZER_FILE - | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT, + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT + | typeof ML_PAGES.AIOPS + | typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES, MlGenericUrlPageState | undefined >; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index eb00ca117f01a..f62cec0ec0fca 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -7,6 +7,7 @@ "ml" ], "requiredPlugins": [ + "aiops", "cloud", "data", "dataViews", diff --git a/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx b/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.tsx new file mode 100644 index 0000000000000..473525d40ca9a --- /dev/null +++ b/x-pack/plugins/ml/public/application/aiops/explain_log_rate_spikes.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, { FC, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { ExplainLogRateSpikesSpec } from '@kbn/aiops-plugin/public'; +import { useMlKibana, useTimefilter } from '../contexts/kibana'; +import { HelpMenu } from '../components/help_menu'; + +import { MlPageHeader } from '../components/page_header'; + +export const ExplainLogRateSpikesPage: FC = () => { + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); + const { + services: { docLinks, aiops }, + } = useMlKibana(); + + const [ExplainLogRateSpikes, setExplainLogRateSpikes] = useState( + null + ); + + useEffect(() => { + if (aiops !== undefined) { + const { getExplainLogRateSpikesComponent } = aiops; + getExplainLogRateSpikesComponent().then(setExplainLogRateSpikes); + } + }, []); + + return ( + <> + {ExplainLogRateSpikes !== null ? ( + <> + + + + + + ) : null} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/aiops/index.ts b/x-pack/plugins/ml/public/application/aiops/index.ts new file mode 100644 index 0000000000000..fa47ae09822e2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/aiops/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { ExplainLogRateSpikesPage } from './explain_log_rate_spikes'; diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 833a4fade128b..50417aafab9b6 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -82,6 +82,7 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { maps: deps.maps, triggersActionsUi: deps.triggersActionsUi, dataVisualizer: deps.dataVisualizer, + aiops: deps.aiops, usageCollection: deps.usageCollection, fieldFormats: deps.fieldFormats, dashboard: deps.dashboard, @@ -135,6 +136,7 @@ export const renderApp = ( dashboard: deps.dashboard, maps: deps.maps, dataVisualizer: deps.dataVisualizer, + aiops: deps.aiops, dataViews: deps.data.dataViews, }); diff --git a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx index 301939fb6fdbc..d41ca59255467 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx @@ -71,7 +71,10 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps ); const routeList = useMemo( - () => Object.values(routes).map((routeFactory) => routeFactory(navigateToPath, basePath.get())), + () => + Object.values(routes) + .map((routeFactory) => routeFactory(navigateToPath, basePath.get())) + .filter((d) => !d.disabled), [] ); diff --git a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx index e5c67de96f494..84474e85330d6 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { EuiSideNavItemType } from '@elastic/eui'; import { useCallback, useMemo } from 'react'; +import { AIOPS_ENABLED } from '@kbn/aiops-plugin/common'; import type { MlLocatorParams } from '../../../../common/types/locator'; import { useUrlState } from '../../util/url_state'; import { useMlLocator, useNavigateToPath } from '../../contexts/kibana'; @@ -64,7 +65,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { const tabsDefinition: Tab[] = useMemo((): Tab[] => { const disableLinks = mlFeaturesDisabled; - return [ + const mlTabs: Tab[] = [ { id: 'main_section', name: '', @@ -218,6 +219,28 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { ], }, ]; + + if (AIOPS_ENABLED) { + mlTabs.push({ + id: 'aiops_section', + name: i18n.translate('xpack.ml.navMenu.aiopsTabLinkText', { + defaultMessage: 'AIOps', + }), + items: [ + { + id: 'explainlogratespikes', + pathId: ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES, + name: i18n.translate('xpack.ml.navMenu.explainLogRateSpikesLinkText', { + defaultMessage: 'Explain log rate spikes', + }), + disabled: disableLinks, + testSubj: 'mlMainTab explainLogRateSpikes', + }, + ], + }); + } + + return mlTabs; }, [mlFeaturesDisabled, canViewMlNodes]); const getTabItem: (tab: Tab) => EuiSideNavItemType = useCallback( diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index bfb27e6d4dbbc..fdfcd9106e8e0 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -16,6 +16,7 @@ import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { MapsStartApi } from '@kbn/maps-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; +import type { AiopsPluginStart } from '@kbn/aiops-plugin/public'; import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; import type { DashboardSetup } from '@kbn/dashboard-plugin/public'; @@ -32,6 +33,7 @@ interface StartPlugins { maps?: MapsStartApi; triggersActionsUi?: TriggersAndActionsUIPublicPluginStart; dataVisualizer?: DataVisualizerPluginStart; + aiops?: AiopsPluginStart; usageCollection?: UsageCollectionSetup; fieldFormats: FieldFormatsRegistry; dashboard: DashboardSetup; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js index 84908775a14a8..339925d3f16ee 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -83,7 +83,8 @@ describe('ExplorerChart', () => { ); // test if the loading indicator is shown - expect(wrapper.find('.ml-loading-indicator .euiLoadingChart')).toHaveLength(1); + // Added span because class appears twice with classNames and Emotion + expect(wrapper.find('.ml-loading-indicator span.euiLoadingChart')).toHaveLength(1); }); // For the following tests the directive needs to be rendered in the actual DOM, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index 890feb6efaf18..3748a196e742d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -86,7 +86,8 @@ describe('ExplorerChart', () => { ); // test if the loading indicator is shown - expect(wrapper.find('.ml-loading-indicator .euiLoadingChart')).toHaveLength(1); + // Added span because class appears twice with classNames and Emotion + expect(wrapper.find('.ml-loading-indicator span.euiLoadingChart')).toHaveLength(1); }); // For the following tests the directive needs to be rendered in the actual DOM, diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts index e563831d16376..54aedb4a71857 100644 --- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts @@ -55,6 +55,13 @@ export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ href: '/datavisualizer', }); +export const AIOPS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ + text: i18n.translate('xpack.ml.aiopsBreadcrumbLabel', { + defaultMessage: 'AIOps', + }), + href: '/aiops', +}); + export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.createJobsBreadcrumbLabel', { defaultMessage: 'Create job', @@ -83,6 +90,7 @@ const breadcrumbs = { DATA_FRAME_ANALYTICS_BREADCRUMB, TRAINED_MODELS, DATA_VISUALIZER_BREADCRUMB, + AIOPS_BREADCRUMB, CREATE_JOB_BREADCRUMB, CALENDAR_MANAGEMENT_BREADCRUMB, FILTER_LISTS_BREADCRUMB, diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index e4e7daa9ee0e1..a761bce2ce38a 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -48,6 +48,7 @@ export interface MlRoute { enableDatePicker?: boolean; 'data-test-subj'?: string; actionMenu?: React.ReactNode; + disabled?: boolean; } export interface PageProps { diff --git a/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx b/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx new file mode 100644 index 0000000000000..ca670df258a6a --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/routes/aiops/explain_log_rate_spikes.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { parse } from 'query-string'; + +import { i18n } from '@kbn/i18n'; + +import { AIOPS_ENABLED } from '@kbn/aiops-plugin/common'; + +import { NavigateToPath } from '../../../contexts/kibana'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { ExplainLogRateSpikesPage as Page } from '../../../aiops/explain_log_rate_spikes'; + +import { checkBasicLicense } from '../../../license'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; +import { cacheDataViewsContract } from '../../../util/index_utils'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const explainLogRateSpikesRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ + id: 'explain_log_rate_spikes', + path: '/aiops/explain_log_rate_spikes', + title: i18n.translate('xpack.ml.aiops.explainLogRateSpikes.docTitle', { + defaultMessage: 'Explain log rate spikes', + }), + render: (props, deps) => , + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB', navigateToPath, basePath), + { + text: i18n.translate('xpack.ml.AiopsBreadcrumbs.explainLogRateSpikesLabel', { + defaultMessage: 'Explain log rate spikes', + }), + }, + ], + disabled: !AIOPS_ENABLED, +}); + +const PageWrapper: FC = ({ location, deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); + const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, { + checkBasicLicense, + cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract), + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + }); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/aiops/index.ts b/x-pack/plugins/ml/public/application/routing/routes/aiops/index.ts new file mode 100644 index 0000000000000..f2b192a4cd097 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/routes/aiops/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 * from './explain_log_rate_spikes'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/index.ts b/x-pack/plugins/ml/public/application/routing/routes/index.ts index 31a8d863e3086..12ddc39e0e23e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/index.ts @@ -11,6 +11,7 @@ export * from './new_job'; export * from './datavisualizer'; export * from './settings'; export * from './data_frame_analytics'; +export * from './aiops'; export { timeSeriesExplorerRouteFactory } from './timeseriesexplorer'; export * from './explorer'; export * from './access_denied'; diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index 3680f8b63b0c9..00895cdb3990e 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -27,6 +27,7 @@ import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import type { SecurityPluginSetup } from '@kbn/security-plugin/public'; import type { MapsStartApi } from '@kbn/maps-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; +import type { AiopsPluginStart } from '@kbn/aiops-plugin/public'; export interface DependencyCache { timefilter: DataPublicPluginSetup['query']['timefilter'] | null; @@ -48,6 +49,7 @@ export interface DependencyCache { dashboard: DashboardStart | null; maps: MapsStartApi | null; dataVisualizer: DataVisualizerPluginStart | null; + aiops: AiopsPluginStart | null; dataViews: DataViewsContract | null; } @@ -71,6 +73,7 @@ const cache: DependencyCache = { dashboard: null, maps: null, dataVisualizer: null, + aiops: null, dataViews: null, }; @@ -93,6 +96,7 @@ export function setDependencyCache(deps: Partial) { cache.i18n = deps.i18n || null; cache.dashboard = deps.dashboard || null; cache.dataVisualizer = deps.dataVisualizer || null; + cache.aiops = deps.aiops || null; cache.dataViews = deps.dataViews || null; } diff --git a/x-pack/plugins/ml/public/locator/ml_locator.ts b/x-pack/plugins/ml/public/locator/ml_locator.ts index b1ea2549c3347..01d63aa0ebf3f 100644 --- a/x-pack/plugins/ml/public/locator/ml_locator.ts +++ b/x-pack/plugins/ml/public/locator/ml_locator.ts @@ -83,6 +83,8 @@ export class MlLocatorDefinition implements LocatorDefinition { case ML_PAGES.DATA_VISUALIZER_FILE: case ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER: case ML_PAGES.DATA_VISUALIZER_INDEX_SELECT: + case ML_PAGES.AIOPS: + case ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES: case ML_PAGES.OVERVIEW: case ML_PAGES.SETTINGS: case ML_PAGES.FILTER_LISTS_MANAGE: diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 1ef7c73d2189a..79f386d521da1 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -37,6 +37,7 @@ import { TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; +import type { AiopsPluginStart } from '@kbn/aiops-plugin/public'; import type { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import type { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/public'; @@ -59,6 +60,7 @@ export interface MlStartDependencies { maps?: MapsStartApi; triggersActionsUi?: TriggersAndActionsUIPublicPluginStart; dataVisualizer: DataVisualizerPluginStart; + aiops: AiopsPluginStart; fieldFormats: FieldFormatsStart; dashboard: DashboardStart; charts: ChartsPluginStart; @@ -125,6 +127,7 @@ export class MlPlugin implements Plugin { kibanaVersion, triggersActionsUi: pluginsStart.triggersActionsUi, dataVisualizer: pluginsStart.dataVisualizer, + aiops: pluginsStart.aiops, usageCollection: pluginsSetup.usageCollection, fieldFormats: pluginsStart.fieldFormats, }, diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index a937586369ef4..bd89d383adcef 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../cloud/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../data_visualizer/tsconfig.json"}, + { "path": "../aiops/tsconfig.json"}, { "path": "../license_management/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap index e3fa9da6639b3..dff498b7f0ccd 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap @@ -5,7 +5,7 @@ exports[`CheckerErrors should render nothing if errors is empty 1`] = `null`; exports[`CheckerErrors should render typical boom errors from api response 1`] = ` Array [
    ,



    , + -
    - -

    - - -1 - , - "property": - xpack.monitoring.collection.enabled - , - } - } - > - We checked the cluster settings and found that - - - xpack.monitoring.collection.enabled - - - is set to - - - -1 - - - . - -

    -

    - - Would you like to turn it on? - + Monitoring provides insight to your hardware performance and load.

    -
    - -
    - - , +
    , +
    +

    + We checked the cluster settings and found that + + xpack.monitoring.collection.enabled + + is set to + + -1 + + . +

    +

    + Would you like to turn it on? +

    +
    , +
    , +
    - -
    - - - - - -
    -
    + Turn on monitoring + + +
    - - +
    , +] `; diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.test.js index 95dc62abdf9d2..d7957dcc457ec 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.test.js @@ -26,7 +26,7 @@ describe('ExplainCollectionEnabled', () => { test('should explain about xpack.monitoring.collection.enabled setting', () => { const rendered = mountWithIntl(component); - expect(rendered).toMatchSnapshot(); + expect(rendered.render()).toMatchSnapshot(); }); test('should have a button that triggers ajax action', () => { diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__snapshots__/collection_interval.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__snapshots__/collection_interval.test.js.snap index dc0253e80fecb..84486fafca89a 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__snapshots__/collection_interval.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__snapshots__/collection_interval.test.js.snap @@ -138,8 +138,91 @@ exports[`ExplainCollectionInterval collection interval setting updates should sh size="half" >
    + css="unknown styles" + > + + + + , + , + ], + }, + } + } + isStringTag={true} + serialized={ + Object { + "map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL2hvcml6b250YWxfcnVsZS9ob3Jpem9udGFsX3J1bGUuc3R5bGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQTRDUSIsImZpbGUiOiIuLi8uLi8uLi9zcmMvY29tcG9uZW50cy9ob3Jpem9udGFsX3J1bGUvaG9yaXpvbnRhbF9ydWxlLnN0eWxlcy50cyIsInNvdXJjZXNDb250ZW50IjpbIi8qXG4gKiBDb3B5cmlnaHQgRWxhc3RpY3NlYXJjaCBCLlYuIGFuZC9vciBsaWNlbnNlZCB0byBFbGFzdGljc2VhcmNoIEIuVi4gdW5kZXIgb25lXG4gKiBvciBtb3JlIGNvbnRyaWJ1dG9yIGxpY2Vuc2UgYWdyZWVtZW50cy4gTGljZW5zZWQgdW5kZXIgdGhlIEVsYXN0aWMgTGljZW5zZVxuICogMi4wIGFuZCB0aGUgU2VydmVyIFNpZGUgUHVibGljIExpY2Vuc2UsIHYgMTsgeW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHRcbiAqIGluIGNvbXBsaWFuY2Ugd2l0aCwgYXQgeW91ciBlbGVjdGlvbiwgdGhlIEVsYXN0aWMgTGljZW5zZSAyLjAgb3IgdGhlIFNlcnZlclxuICogU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxLlxuICovXG5cbmltcG9ydCB7IGNzcyB9IGZyb20gJ0BlbW90aW9uL3JlYWN0JztcbmltcG9ydCB7IFVzZUV1aVRoZW1lIH0gZnJvbSAnLi4vLi4vc2VydmljZXMnO1xuXG5leHBvcnQgY29uc3QgZXVpSG9yaXpvbnRhbFJ1bGVTdHlsZXMgPSAoeyBldWlUaGVtZSB9OiBVc2VFdWlUaGVtZSkgPT4gKHtcbiAgZXVpSG9yaXpvbnRhbFJ1bGU6IGNzc2BcbiAgICBib3JkZXI6IG5vbmU7XG4gICAgaGVpZ2h0OiAke2V1aVRoZW1lLmJvcmRlci53aWR0aC50aGlufTtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAke2V1aVRoZW1lLmJvcmRlci5jb2xvcn07XG4gICAgZmxleC1zaHJpbms6IDA7IC8vIEVuc3VyZSB3aGVuIHVzZWQgaW4gZmxleCBncm91cCwgaXQgcmV0YWlucyBpdHMgc2l6ZVxuICAgIGZsZXgtZ3JvdzogMDsgLy8gRW5zdXJlIHdoZW4gdXNlZCBpbiBmbGV4IGdyb3VwLCBpdCByZXRhaW5zIGl0cyBzaXplXG4gIGAsXG5cbiAgLy8gU2l6ZXNcbiAgZnVsbDogY3NzYFxuICAgIHdpZHRoOiAxMDAlO1xuICBgLFxuICBoYWxmOiBjc3NgXG4gICAgd2lkdGg6IDUwJTtcbiAgICBtYXJnaW4taW5saW5lOiBhdXRvO1xuICBgLFxuICBxdWFydGVyOiBjc3NgXG4gICAgd2lkdGg6IDI1JTtcbiAgICBtYXJnaW4taW5saW5lOiBhdXRvO1xuICBgLFxuXG4gIC8vIE1hcmdpbnNcbiAgbm9uZTogJycsXG4gIHhzOiBjc3NgXG4gICAgbWFyZ2luLWJsb2NrOiAke2V1aVRoZW1lLnNpemUuc307XG4gIGAsXG4gIHM6IGNzc2BcbiAgICBtYXJnaW4tYmxvY2s6ICR7ZXVpVGhlbWUuc2l6ZS5tfTtcbiAgYCxcbiAgbTogY3NzYFxuICAgIG1hcmdpbi1ibG9jazogJHtldWlUaGVtZS5zaXplLmJhc2V9O1xuICBgLFxuICBsOiBjc3NgXG4gICAgbWFyZ2luLWJsb2NrOiAke2V1aVRoZW1lLnNpemUubH07XG4gIGAsXG4gIHhsOiBjc3NgXG4gICAgbWFyZ2luLWJsb2NrOiAke2V1aVRoZW1lLnNpemUueGx9O1xuICBgLFxuICB4eGw6IGNzc2BcbiAgICBtYXJnaW4tYmxvY2s6ICR7ZXVpVGhlbWUuc2l6ZS54eGx9O1xuICBgLFxufSk7XG4iXX0= */", + "name": "ilegow-euiHorizontalRule-half-l", + "next": undefined, + "styles": "border:none;height:1px;background-color:#D3DAE6;flex-shrink:0;flex-grow:0;;label:euiHorizontalRule;;;width:50%;margin-inline:auto;label:half;;;margin-block:24px;;label:l;;;", + "toString": [Function], + } + } + /> +
    +
    + css="unknown styles" + > + + + + , + , + ], + }, + } + } + isStringTag={true} + serialized={ + Object { + "map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL3NwYWNlci9zcGFjZXIuc3R5bGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQTBCUSIsImZpbGUiOiIuLi8uLi8uLi9zcmMvY29tcG9uZW50cy9zcGFjZXIvc3BhY2VyLnN0eWxlcy50cyIsInNvdXJjZXNDb250ZW50IjpbIi8qXG4gKiBDb3B5cmlnaHQgRWxhc3RpY3NlYXJjaCBCLlYuIGFuZC9vciBsaWNlbnNlZCB0byBFbGFzdGljc2VhcmNoIEIuVi4gdW5kZXIgb25lXG4gKiBvciBtb3JlIGNvbnRyaWJ1dG9yIGxpY2Vuc2UgYWdyZWVtZW50cy4gTGljZW5zZWQgdW5kZXIgdGhlIEVsYXN0aWMgTGljZW5zZVxuICogMi4wIGFuZCB0aGUgU2VydmVyIFNpZGUgUHVibGljIExpY2Vuc2UsIHYgMTsgeW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHRcbiAqIGluIGNvbXBsaWFuY2Ugd2l0aCwgYXQgeW91ciBlbGVjdGlvbiwgdGhlIEVsYXN0aWMgTGljZW5zZSAyLjAgb3IgdGhlIFNlcnZlclxuICogU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxLlxuICovXG5cbmltcG9ydCB7IGNzcyB9IGZyb20gJ0BlbW90aW9uL3JlYWN0JztcbmltcG9ydCB7IFVzZUV1aVRoZW1lIH0gZnJvbSAnLi4vLi4vc2VydmljZXMnO1xuXG5leHBvcnQgY29uc3QgZXVpU3BhY2VyU3R5bGVzID0gKHsgZXVpVGhlbWUgfTogVXNlRXVpVGhlbWUpID0+ICh7XG4gIC8vIGJhc2VcbiAgZXVpU3BhY2VyOiBjc3NgXG4gICAgZmxleC1zaHJpbms6IDA7IC8vIGRvbid0IGV2ZXIgbGV0IHRoaXMgc2hyaW5rIGluIGhlaWdodCBpZiBkaXJlY3QgZGVzY2VuZGVudCBvZiBmbGV4O1xuICBgLFxuICAvLyB2YXJpYW50c1xuICB4czogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnhzfTtcbiAgYCxcbiAgczogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnN9O1xuICBgLFxuICBtOiBjc3NgXG4gICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemUuYmFzZX07XG4gIGAsXG4gIGw6IGNzc2BcbiAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZS5sfTtcbiAgYCxcbiAgeGw6IGNzc2BcbiAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZS54bH07XG4gIGAsXG4gIHh4bDogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnh4bH07XG4gIGAsXG59KTtcbiJdfQ== */", + "name": "jz428s-euiSpacer-l", + "next": undefined, + "styles": "flex-shrink:0;label:euiSpacer;;;height:24px;;label:l;;;", + "toString": [Function], + } + } + /> +
    +

    + css="unknown styles" + > + + + + , + , + ], + }, + } + } + isStringTag={true} + serialized={ + Object { + "map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL2hvcml6b250YWxfcnVsZS9ob3Jpem9udGFsX3J1bGUuc3R5bGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQTRDUSIsImZpbGUiOiIuLi8uLi8uLi9zcmMvY29tcG9uZW50cy9ob3Jpem9udGFsX3J1bGUvaG9yaXpvbnRhbF9ydWxlLnN0eWxlcy50cyIsInNvdXJjZXNDb250ZW50IjpbIi8qXG4gKiBDb3B5cmlnaHQgRWxhc3RpY3NlYXJjaCBCLlYuIGFuZC9vciBsaWNlbnNlZCB0byBFbGFzdGljc2VhcmNoIEIuVi4gdW5kZXIgb25lXG4gKiBvciBtb3JlIGNvbnRyaWJ1dG9yIGxpY2Vuc2UgYWdyZWVtZW50cy4gTGljZW5zZWQgdW5kZXIgdGhlIEVsYXN0aWMgTGljZW5zZVxuICogMi4wIGFuZCB0aGUgU2VydmVyIFNpZGUgUHVibGljIExpY2Vuc2UsIHYgMTsgeW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHRcbiAqIGluIGNvbXBsaWFuY2Ugd2l0aCwgYXQgeW91ciBlbGVjdGlvbiwgdGhlIEVsYXN0aWMgTGljZW5zZSAyLjAgb3IgdGhlIFNlcnZlclxuICogU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxLlxuICovXG5cbmltcG9ydCB7IGNzcyB9IGZyb20gJ0BlbW90aW9uL3JlYWN0JztcbmltcG9ydCB7IFVzZUV1aVRoZW1lIH0gZnJvbSAnLi4vLi4vc2VydmljZXMnO1xuXG5leHBvcnQgY29uc3QgZXVpSG9yaXpvbnRhbFJ1bGVTdHlsZXMgPSAoeyBldWlUaGVtZSB9OiBVc2VFdWlUaGVtZSkgPT4gKHtcbiAgZXVpSG9yaXpvbnRhbFJ1bGU6IGNzc2BcbiAgICBib3JkZXI6IG5vbmU7XG4gICAgaGVpZ2h0OiAke2V1aVRoZW1lLmJvcmRlci53aWR0aC50aGlufTtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAke2V1aVRoZW1lLmJvcmRlci5jb2xvcn07XG4gICAgZmxleC1zaHJpbms6IDA7IC8vIEVuc3VyZSB3aGVuIHVzZWQgaW4gZmxleCBncm91cCwgaXQgcmV0YWlucyBpdHMgc2l6ZVxuICAgIGZsZXgtZ3JvdzogMDsgLy8gRW5zdXJlIHdoZW4gdXNlZCBpbiBmbGV4IGdyb3VwLCBpdCByZXRhaW5zIGl0cyBzaXplXG4gIGAsXG5cbiAgLy8gU2l6ZXNcbiAgZnVsbDogY3NzYFxuICAgIHdpZHRoOiAxMDAlO1xuICBgLFxuICBoYWxmOiBjc3NgXG4gICAgd2lkdGg6IDUwJTtcbiAgICBtYXJnaW4taW5saW5lOiBhdXRvO1xuICBgLFxuICBxdWFydGVyOiBjc3NgXG4gICAgd2lkdGg6IDI1JTtcbiAgICBtYXJnaW4taW5saW5lOiBhdXRvO1xuICBgLFxuXG4gIC8vIE1hcmdpbnNcbiAgbm9uZTogJycsXG4gIHhzOiBjc3NgXG4gICAgbWFyZ2luLWJsb2NrOiAke2V1aVRoZW1lLnNpemUuc307XG4gIGAsXG4gIHM6IGNzc2BcbiAgICBtYXJnaW4tYmxvY2s6ICR7ZXVpVGhlbWUuc2l6ZS5tfTtcbiAgYCxcbiAgbTogY3NzYFxuICAgIG1hcmdpbi1ibG9jazogJHtldWlUaGVtZS5zaXplLmJhc2V9O1xuICBgLFxuICBsOiBjc3NgXG4gICAgbWFyZ2luLWJsb2NrOiAke2V1aVRoZW1lLnNpemUubH07XG4gIGAsXG4gIHhsOiBjc3NgXG4gICAgbWFyZ2luLWJsb2NrOiAke2V1aVRoZW1lLnNpemUueGx9O1xuICBgLFxuICB4eGw6IGNzc2BcbiAgICBtYXJnaW4tYmxvY2s6ICR7ZXVpVGhlbWUuc2l6ZS54eGx9O1xuICBgLFxufSk7XG4iXX0= */", + "name": "ilegow-euiHorizontalRule-half-l", + "next": undefined, + "styles": "border:none;height:1px;background-color:#D3DAE6;flex-shrink:0;flex-grow:0;;label:euiHorizontalRule;;;width:50%;margin-inline:auto;label:half;;;margin-block:24px;;label:l;;;", + "toString": [Function], + } + } + /> +
    +
    + css="unknown styles" + > + + + + , + , + ], + }, + } + } + isStringTag={true} + serialized={ + Object { + "map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL3NwYWNlci9zcGFjZXIuc3R5bGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQTBCUSIsImZpbGUiOiIuLi8uLi8uLi9zcmMvY29tcG9uZW50cy9zcGFjZXIvc3BhY2VyLnN0eWxlcy50cyIsInNvdXJjZXNDb250ZW50IjpbIi8qXG4gKiBDb3B5cmlnaHQgRWxhc3RpY3NlYXJjaCBCLlYuIGFuZC9vciBsaWNlbnNlZCB0byBFbGFzdGljc2VhcmNoIEIuVi4gdW5kZXIgb25lXG4gKiBvciBtb3JlIGNvbnRyaWJ1dG9yIGxpY2Vuc2UgYWdyZWVtZW50cy4gTGljZW5zZWQgdW5kZXIgdGhlIEVsYXN0aWMgTGljZW5zZVxuICogMi4wIGFuZCB0aGUgU2VydmVyIFNpZGUgUHVibGljIExpY2Vuc2UsIHYgMTsgeW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHRcbiAqIGluIGNvbXBsaWFuY2Ugd2l0aCwgYXQgeW91ciBlbGVjdGlvbiwgdGhlIEVsYXN0aWMgTGljZW5zZSAyLjAgb3IgdGhlIFNlcnZlclxuICogU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxLlxuICovXG5cbmltcG9ydCB7IGNzcyB9IGZyb20gJ0BlbW90aW9uL3JlYWN0JztcbmltcG9ydCB7IFVzZUV1aVRoZW1lIH0gZnJvbSAnLi4vLi4vc2VydmljZXMnO1xuXG5leHBvcnQgY29uc3QgZXVpU3BhY2VyU3R5bGVzID0gKHsgZXVpVGhlbWUgfTogVXNlRXVpVGhlbWUpID0+ICh7XG4gIC8vIGJhc2VcbiAgZXVpU3BhY2VyOiBjc3NgXG4gICAgZmxleC1zaHJpbms6IDA7IC8vIGRvbid0IGV2ZXIgbGV0IHRoaXMgc2hyaW5rIGluIGhlaWdodCBpZiBkaXJlY3QgZGVzY2VuZGVudCBvZiBmbGV4O1xuICBgLFxuICAvLyB2YXJpYW50c1xuICB4czogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnhzfTtcbiAgYCxcbiAgczogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnN9O1xuICBgLFxuICBtOiBjc3NgXG4gICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemUuYmFzZX07XG4gIGAsXG4gIGw6IGNzc2BcbiAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZS5sfTtcbiAgYCxcbiAgeGw6IGNzc2BcbiAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZS54bH07XG4gIGAsXG4gIHh4bDogY3NzYFxuICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplLnh4bH07XG4gIGAsXG59KTtcbiJdfQ== */", + "name": "jz428s-euiSpacer-l", + "next": undefined, + "styles": "flex-shrink:0;label:euiSpacer;;;height:24px;;label:l;;;", + "toString": [Function], + } + } + /> +
    +
    - - -

    - - Monitoring is currently off - -

    -
    - - - -
    -

    - - Monitoring provides insight to your hardware performance and load. - -

    -
    -
    -
    -
    -
    - + Monitoring is currently off + , + -
    -
    -

    - - -1 - , - "property": - xpack.monitoring.collection.interval - , - } - } - > - We checked the cluster settings and found that - - - xpack.monitoring.collection.interval - - - is set to - - - -1 - - - . - -

    -

    - - The collection interval setting needs to be a positive integer (10s is recommended) in order for the collection agents to be active. - -

    -

    - - Would you like us to change it and enable monitoring? - + Monitoring provides insight to your hardware performance and load.

    -
    - -
    - - , +
    , +
    +

    + We checked the cluster settings and found that + + xpack.monitoring.collection.interval + + is set to + + -1 + + . +

    +

    + The collection interval setting needs to be a positive integer (10s is recommended) in order for the collection agents to be active. +

    +

    + Would you like us to change it and enable monitoring? +

    +
    , +
    , +
    - -
    - - - - - -
    -
    + Turn on monitoring + + +
    - - +
    , +] `; diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.test.js index 95ffad81b902d..4b7af5e22f1d7 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.test.js @@ -32,7 +32,7 @@ describe('ExplainCollectionInterval', () => { /> ); const rendered = mountWithIntl(component); - expect(rendered).toMatchSnapshot(); + expect(rendered.render()).toMatchSnapshot(); }); test('should have a button that triggers ajax action', () => { diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap index 41501a7eedb62..494985be0a6bf 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__snapshots__/exporters.test.js.snap @@ -19,7 +19,7 @@ Array [
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,

    { observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), ObservabilityPageTemplate: KibanaPageTemplate, kibanaFeatures: [], + usageCollection: { + components: { + ApplicationUsageTrackingProvider: (props) => null, + }, + reportUiCounter: jest.fn(), + }, }); unmount(); }).not.toThrowError(); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index bab6c03b5cf54..c48a663fefe5b 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -18,6 +18,7 @@ import { RedirectAppLinks, } from '@kbn/kibana-react-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ConfigSchema } from '..'; import type { LazyObservabilityPageTemplateProps } from '../components/shared/page_template/lazy_page_template'; import { DatePickerContextProvider } from '../context/date_picker_context'; @@ -54,6 +55,7 @@ export const renderApp = ({ observabilityRuleTypeRegistry, ObservabilityPageTemplate, kibanaFeatures, + usageCollection, }: { config: ConfigSchema; core: CoreStart; @@ -62,6 +64,7 @@ export const renderApp = ({ appMountParameters: AppMountParameters; ObservabilityPageTemplate: React.ComponentType; kibanaFeatures: KibanaFeature[]; + usageCollection: UsageCollectionSetup; }) => { const { element, history, theme$ } = appMountParameters; const i18nCore = core.i18n; @@ -77,34 +80,40 @@ export const renderApp = ({ // ensure all divs are .kbnAppWrappers element.classList.add(APP_WRAPPER_CLASS); + const ApplicationUsageTrackingProvider = + usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; ReactDOM.render( - - - + + - - - - - - - - - - - - - - - - , + + + + + + + + + + + + + + + + + + , element ); return () => { diff --git a/x-pack/plugins/observability/public/config/index.ts b/x-pack/plugins/observability/public/config/index.ts index fc6300acc4716..34d783180750b 100644 --- a/x-pack/plugins/observability/public/config/index.ts +++ b/x-pack/plugins/observability/public/config/index.ts @@ -7,3 +7,9 @@ export { paths } from './paths'; export { translations } from './translations'; + +export enum AlertingPages { + alerts = 'alerts', + cases = 'cases', + rules = 'rules', +} diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts index de0127b08213e..229a54c754e4f 100644 --- a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts @@ -7,9 +7,9 @@ import { useEffect, useState, useCallback } from 'react'; import { isEmpty } from 'lodash'; -import { loadRules } from '@kbn/triggers-actions-ui-plugin/public'; -import { RULES_LOAD_ERROR } from '../pages/rules/translations'; -import { FetchRulesProps, RuleState } from '../pages/rules/types'; +import { loadRules, loadRuleTags } from '@kbn/triggers-actions-ui-plugin/public'; +import { RULES_LOAD_ERROR, RULE_TAGS_LOAD_ERROR } from '../pages/rules/translations'; +import { FetchRulesProps, RuleState, TagsState } from '../pages/rules/types'; import { OBSERVABILITY_RULE_TYPES } from '../pages/rules/config'; import { useKibana } from '../utils/kibana_react'; @@ -18,6 +18,7 @@ export function useFetchRules({ ruleLastResponseFilter, ruleStatusesFilter, typesFilter, + tagsFilter, setPage, page, sort, @@ -33,6 +34,23 @@ export function useFetchRules({ const [noData, setNoData] = useState(true); const [initialLoad, setInitialLoad] = useState(true); + const [tagsState, setTagsState] = useState({ + data: [], + error: null, + }); + const loadRuleTagsAggs = useCallback(async () => { + try { + const ruleTagsAggs = await loadRuleTags({ + http, + }); + + if (ruleTagsAggs?.ruleTags) { + setTagsState({ data: ruleTagsAggs.ruleTags, error: null }); + } + } catch (e) { + setTagsState((oldState: TagsState) => ({ ...oldState, error: RULE_TAGS_LOAD_ERROR })); + } + }, [http]); const fetchRules = useCallback(async () => { setRulesState((oldState) => ({ ...oldState, isLoading: true })); @@ -43,10 +61,12 @@ export function useFetchRules({ page, searchText, typesFilter: typesFilter.length > 0 ? typesFilter : OBSERVABILITY_RULE_TYPES, + tagsFilter, ruleExecutionStatusesFilter: ruleLastResponseFilter, ruleStatusesFilter, sort, }); + await loadRuleTagsAggs(); setRulesState((oldState) => ({ ...oldState, isLoading: false, @@ -60,8 +80,9 @@ export function useFetchRules({ const isFilterApplied = !( isEmpty(searchText) && isEmpty(ruleLastResponseFilter) && - isEmpty(ruleStatusesFilter) && - isEmpty(typesFilter) + isEmpty(typesFilter) && + isEmpty(tagsFilter) && + isEmpty(ruleStatusesFilter) ); setNoData(response.data.length === 0 && !isFilterApplied); @@ -75,6 +96,8 @@ export function useFetchRules({ setPage, searchText, ruleLastResponseFilter, + tagsFilter, + loadRuleTagsAggs, ruleStatusesFilter, typesFilter, sort, @@ -89,5 +112,6 @@ export function useFetchRules({ setRulesState, noData, initialLoad, + tagsState, }; } diff --git a/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts b/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts index 50836859a5415..ab1f769c1c4b9 100644 --- a/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts +++ b/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts @@ -39,6 +39,7 @@ const triggersActionsUiStartMock = { getRuleStatusDropdown: jest.fn(), getRuleTagBadge: jest.fn(), getRuleStatusFilter: jest.fn(), + getRuleTagFilter: jest.fn(), ruleTypeRegistry: { has: jest.fn(), register: jest.fn(), diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index 2fe114771c329..8838ccd2ac56f 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -284,6 +284,7 @@ function AlertsPage() { setRefetch={setRefetch} stateStorageKey={ALERT_TABLE_STATE_STORAGE_KEY} storage={new Storage(window.localStorage)} + itemsPerPage={50} /> diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx index 686ae9a15d8de..6ae011e35b0b2 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx @@ -78,6 +78,7 @@ interface AlertsTableTGridProps { stateStorageKey: string; storage: IStorageWrapper; setRefetch: (ref: () => void) => void; + itemsPerPage?: number; } interface ObservabilityActionsProps extends ActionProps { @@ -313,7 +314,16 @@ const FIELDS_WITHOUT_CELL_ACTIONS = [ ]; export function AlertsTableTGrid(props: AlertsTableTGridProps) { - const { indexNames, rangeFrom, rangeTo, kuery, setRefetch, stateStorageKey, storage } = props; + const { + indexNames, + rangeFrom, + rangeTo, + kuery, + setRefetch, + stateStorageKey, + storage, + itemsPerPage, + } = props; const { timelines, @@ -409,6 +419,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { filters: [], hasAlertsCrudPermissions, indexNames, + itemsPerPage, itemsPerPageOptions: [10, 25, 50], loadingText: translations.alertsTable.loadingTextLabel, footerText: translations.alertsTable.footerTextLabel, @@ -459,6 +470,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { deletedEventIds, onStateChange, tGridState, + itemsPerPage, ]); const handleFlyoutClose = () => setFlyoutAlert(undefined); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 9ceabf7c3111a..6d95db9d7694f 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -66,6 +66,7 @@ function calculateBucketSize({ start, end }: { start?: number; end?: number }) { } const ALERT_TABLE_STATE_STORAGE_KEY = 'xpack.observability.overview.alert.tableState'; +const ALERTS_PER_PAGE = 10; export function OverviewPage({ routeParams }: Props) { const trackMetric = useUiTracker({ app: 'observability-overview' }); @@ -208,6 +209,7 @@ export function OverviewPage({ routeParams }: Props) { rangeFrom={relativeStart} rangeTo={relativeEnd} indexNames={indexNames} + itemsPerPage={ALERTS_PER_PAGE} stateStorageKey={ALERT_TABLE_STATE_STORAGE_KEY} storage={new Storage(window.localStorage)} /> diff --git a/x-pack/plugins/observability/public/pages/rules/index.test.tsx b/x-pack/plugins/observability/public/pages/rules/index.test.tsx index ac31587d74702..6987026b3b9bd 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.test.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.test.tsx @@ -86,7 +86,11 @@ describe('empty RulesPage', () => { }, ], }); - useFetchRules.mockReturnValue({ rulesState, noData: true }); + useFetchRules.mockReturnValue({ + rulesState, + noData: true, + tagsState: { data: [], error: null }, + }); wrapper = mountWithIntl(); } it('renders empty screen', async () => { @@ -138,7 +142,11 @@ describe('empty RulesPage with show only capability', () => { ruleTaskTimeout: '1m', }, ]; - useFetchRules.mockReturnValue({ rulesState, noData: true }); + useFetchRules.mockReturnValue({ + rulesState, + noData: true, + tagsState: { data: [], error: null }, + }); useLoadRuleTypes.mockReturnValue({ ruleTypes }); wrapper = mountWithIntl(); @@ -352,7 +360,7 @@ describe('RulesPage with items', () => { ruleTypes, ruleTypeIndex: mockedRuleTypeIndex, }); - useFetchRules.mockReturnValue({ rulesState }); + useFetchRules.mockReturnValue({ rulesState, tagsState: { data: [], error: null } }); wrapper = mountWithIntl(); await act(async () => { await nextTick(); @@ -509,7 +517,7 @@ describe('RulesPage with items and show only capability', () => { error: null, totalItemCount: 3, }; - useFetchRules.mockReturnValue({ rulesState }); + useFetchRules.mockReturnValue({ rulesState, tagsState: { data: [], error: null } }); const mockedRuleTypeIndex = new Map( Object.entries({ diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index a409754a51a14..4ab0790cf5bd4 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { capitalize, sortBy } from 'lodash'; import { EuiButton, @@ -93,6 +93,7 @@ function RulesPage() { }); const [inputText, setInputText] = useState(); const [searchText, setSearchText] = useState(); + const [tagsFilter, setTagsFilter] = useState([]); const [typesFilter, setTypesFilter] = useState([]); const { lastResponse, setLastResponse } = useRulesPageStateContainer(); const { status, setStatus } = useRulesPageStateContainer(); @@ -108,16 +109,19 @@ function RulesPage() { setCurrentRuleToEdit(ruleItem); }; - const { rulesState, setRulesState, reload, noData, initialLoad } = useFetchRules({ + const { rulesState, setRulesState, reload, noData, initialLoad, tagsState } = useFetchRules({ searchText, ruleLastResponseFilter: lastResponse, ruleStatusesFilter: status, typesFilter, + tagsFilter, page, setPage, sort, }); const { data: rules, totalItemCount, error } = rulesState; + const { data: tags, error: tagsError } = tagsState; + const { ruleTypeIndex, ruleTypes } = useLoadRuleTypes({ filteredSolutions: OBSERVABILITY_SOLUTIONS, }); @@ -165,6 +169,18 @@ function RulesPage() { }, ]); + useEffect(() => { + if (tagsError) { + toasts.addDanger({ + title: tagsError, + }); + } + if (error) + toasts.addDanger({ + title: error, + }); + }, [tagsError, error, toasts]); + const getRulesTableColumns = () => { return [ { @@ -182,11 +198,11 @@ function RulesPage() { sortable: false, width: '50px', 'data-test-subj': 'rulesTableCell-tagsPopover', - render: (tags: string[], item: RuleTableItem) => { - return tags.length > 0 + render: (ruleTags: string[], item: RuleTableItem) => { + return ruleTags.length > 0 ? triggersActionsUi.getRuleTagBadge({ isOpen: tagPopoverOpenIndex === item.index, - tags, + tags: ruleTags, onClick: () => setTagPopoverOpenIndex(item.index), onClose: () => setTagPopoverOpenIndex(-1), }) @@ -352,6 +368,13 @@ function RulesPage() { )} /> + + {triggersActionsUi.getRuleTagFilter({ + tags, + selectedTags: tagsFilter, + onChange: (myTags: string[]) => setTagsFilter(myTags), + })} + ); }; - return ( {getRulesTable()} - {error && - toasts.addDanger({ - title: error, - })} {currentRuleToEdit && } {createRuleFlyoutVisibility && CreateRuleFlyout} diff --git a/x-pack/plugins/observability/public/pages/rules/translations.ts b/x-pack/plugins/observability/public/pages/rules/translations.ts index 69f0b5beebf46..8484637e25e60 100644 --- a/x-pack/plugins/observability/public/pages/rules/translations.ts +++ b/x-pack/plugins/observability/public/pages/rules/translations.ts @@ -125,6 +125,13 @@ export const RULES_LOAD_ERROR = i18n.translate('xpack.observability.rules.loadEr defaultMessage: 'Unable to load rules', }); +export const RULE_TAGS_LOAD_ERROR = i18n.translate( + 'xpack.observability.rulesList.unableToLoadRuleTags', + { + defaultMessage: 'Unable to load rule tags', + } +); + export const RULES_SINGLE_TITLE = i18n.translate( 'xpack.observability.rules.rulesTable.singleTitle', { diff --git a/x-pack/plugins/observability/public/pages/rules/types.ts b/x-pack/plugins/observability/public/pages/rules/types.ts index 866e5de91d9c5..f7abdc6fd274e 100644 --- a/x-pack/plugins/observability/public/pages/rules/types.ts +++ b/x-pack/plugins/observability/public/pages/rules/types.ts @@ -44,6 +44,7 @@ export interface FetchRulesProps { ruleLastResponseFilter: string[]; ruleStatusesFilter: RuleStatus[]; typesFilter: string[]; + tagsFilter: string[]; page: Pagination; setPage: Dispatch>; sort: EuiTableSortingType['sort']; @@ -66,3 +67,8 @@ export interface RuleState { error: string | null; totalItemCount: number; } + +export interface TagsState { + data: string[]; + error: string | null; +} diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index cb8dcaf2dd7e4..434bce3c576bf 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -33,6 +33,7 @@ import { } from '@kbn/triggers-actions-ui-plugin/public'; import { KibanaFeature } from '@kbn/features-plugin/common'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ConfigSchema } from '.'; import { observabilityAppId, observabilityFeatureId, casesPath } from '../common'; import { createLazyObservabilityPageTemplate } from './components/shared'; @@ -52,9 +53,11 @@ export interface ObservabilityPublicPluginsSetup { data: DataPublicPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; home?: HomePublicPluginSetup; + usageCollection: UsageCollectionSetup; } export interface ObservabilityPublicPluginsStart { + usageCollection: UsageCollectionSetup; cases: CasesUiStart; embeddable: EmbeddableStart; home?: HomePublicPluginStart; @@ -169,6 +172,7 @@ export class Plugin observabilityRuleTypeRegistry, ObservabilityPageTemplate: navigation.PageTemplate, kibanaFeatures, + usageCollection: pluginsSetup.usageCollection, }); }; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 528dbfee06f9d..573eb0b7308e4 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import React from 'react'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { casesPath } from '../../common'; import { CasesPage } from '../pages/cases'; import { AlertsPage } from '../pages/alerts/containers/alerts_page'; @@ -16,6 +17,7 @@ import { OverviewPage } from '../pages/overview'; import { jsonRt } from './json_rt'; import { ObservabilityExploratoryView } from '../components/shared/exploratory_view/obsv_exploratory_view'; import { RulesPage } from '../pages/rules'; +import { AlertingPages } from '../config'; export type RouteParams = DecodeParams; @@ -60,14 +62,22 @@ export const routes = { }, [casesPath]: { handler: () => { - return ; + return ( + + + + ); }, params: {}, exact: false, }, '/alerts': { handler: () => { - return ; + return ( + + + + ); }, params: { // Technically gets a '_a' param by using Kibana URL state sync helpers @@ -90,7 +100,11 @@ export const routes = { }, '/alerts/rules': { handler: () => { - return ; + return ( + + + + ); }, params: {}, exact: true, diff --git a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap index 34825e0511707..1f9c47145d715 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap +++ b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap @@ -1,1256 +1,554 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout option 1`] = ` -
    - -
    -

    - - - Analytical Apps can take a minute or two to generate based upon the size of your test-object-type. - - -

    -
    -
    - -
    - - - } - labelType="label" +

    + + Analytical Apps can take a minute or two to generate based upon the size of your test-object-type. + +

    +
    +
    +
    +
    -
    - - } - onBlur={[Function]} - onChange={[Function]} - onFocus={[Function]} - > -
    - - - - - Full page layout - - - -
    -
    - -
    - - - Remove borders and footer logo - - -
    -
    -
    -
    - - - - - - -
    -
    - + + + Full page layout + + +
    -
    - - - - -
    -
    - -
    -
    - -
    - - -
    -

    - - - Alternatively, copy this POST URL to call generation from outside Kibana or from Watcher. - - -

    -
    -
    - -
    - - - -
    -
    - - - Unsaved work - -
    - -
    - -
    - -
    -

    - - - Save your work before copying this URL. - - -

    -
    -
    - -
    - -
    - -
    - -
    - - -
    -
    - -
    + + Remove borders and footer logo +
    - +
    - -`; - -exports[`ScreenCapturePanelContent properly renders a view with "print" layout option 1`] = ` - -
    - -
    -

    - - - Analytical Apps can take a minute or two to generate based upon the size of your test-object-type. - - -

    -
    -
    - + Generate Analytical App + + + + +
    +
    +
    -
    - -