diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json index e8509e8126adb..6e200caf62ab1 100644 --- a/.buildkite/package-lock.json +++ b/.buildkite/package-lock.json @@ -8,7 +8,7 @@ "name": "kibana-buildkite", "version": "1.0.0", "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#a0037514b7650296a23dbad99b165601d4eab1be" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#ae4994aba5f2e72edcc5914e2aa208086e4b7ea3" } }, "node_modules/@nodelib/fs.scandir": { @@ -368,8 +368,8 @@ }, "node_modules/kibana-buildkite-library": { "version": "1.0.0", - "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#a0037514b7650296a23dbad99b165601d4eab1be", - "integrity": "sha512-W9oH2c0q21IbO3sKJR2BkebhDlXVuWfqKO1r6T/E8/RRxCXJg/Wf073k8aDdpl1Enk8Pq47F+lG7/IVT+kAcFA==", + "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#ae4994aba5f2e72edcc5914e2aa208086e4b7ea3", + "integrity": "sha512-zvMwrJZ7kytbV/rFLMrcKlHGLxrR5G9+mzNqBwvCb0+RIfZ3Kp2IbPkPxqimps/2ipjWTqg92UMv0cGsZedbYQ==", "license": "MIT", "dependencies": { "@octokit/rest": "^18.10.0", @@ -839,9 +839,9 @@ } }, "kibana-buildkite-library": { - "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#a0037514b7650296a23dbad99b165601d4eab1be", - "integrity": "sha512-W9oH2c0q21IbO3sKJR2BkebhDlXVuWfqKO1r6T/E8/RRxCXJg/Wf073k8aDdpl1Enk8Pq47F+lG7/IVT+kAcFA==", - "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#a0037514b7650296a23dbad99b165601d4eab1be", + "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#ae4994aba5f2e72edcc5914e2aa208086e4b7ea3", + "integrity": "sha512-zvMwrJZ7kytbV/rFLMrcKlHGLxrR5G9+mzNqBwvCb0+RIfZ3Kp2IbPkPxqimps/2ipjWTqg92UMv0cGsZedbYQ==", + "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#ae4994aba5f2e72edcc5914e2aa208086e4b7ea3", "requires": { "@octokit/rest": "^18.10.0", "axios": "^0.21.4", diff --git a/.buildkite/package.json b/.buildkite/package.json index 7f15a2fdf75bc..163da13eb0aa5 100644 --- a/.buildkite/package.json +++ b/.buildkite/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#a0037514b7650296a23dbad99b165601d4eab1be" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#ae4994aba5f2e72edcc5914e2aa208086e4b7ea3" } } diff --git a/.buildkite/pipelines/artifacts.yml b/.buildkite/pipelines/artifacts.yml index eac479d4d450f..5f3b0dac3af06 100644 --- a/.buildkite/pipelines/artifacts.yml +++ b/.buildkite/pipelines/artifacts.yml @@ -41,7 +41,17 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/artifacts/docker_context.sh + - command: KIBANA_DOCKER_CONTEXT=default .buildkite/scripts/steps/artifacts/docker_context.sh + label: 'Docker Context Verification' + agents: + queue: n2-2 + timeout_in_minutes: 30 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: KIBANA_DOCKER_CONTEXT=cloud .buildkite/scripts/steps/artifacts/docker_context.sh label: 'Docker Context Verification' agents: queue: n2-2 diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 586199f082925..512f0a2c279a3 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -107,6 +107,16 @@ steps: - exit_status: '-1' limit: 3 + - command: .buildkite/scripts/steps/bazel_cache/bootstrap_linux.sh + label: 'Populate local dev bazel cache (Linux)' + agents: + queue: n2-4-spot + timeout_in_minutes: 15 + retry: + automatic: + - exit_status: '-1' + limit: 3 + - wait: ~ continue_on_failure: true 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/setup_bazel.sh b/.buildkite/scripts/common/setup_bazel.sh index e3791dfa393c7..40159ba9eaf69 100755 --- a/.buildkite/scripts/common/setup_bazel.sh +++ b/.buildkite/scripts/common/setup_bazel.sh @@ -32,6 +32,15 @@ cat <> $KIBANA_DIR/.bazelrc EOF fi +if [[ "$BAZEL_CACHE_MODE" == "populate-local-gcs" ]]; then + echo "[bazel] enabling caching with GCS buckets for local dev" + +cat <> $KIBANA_DIR/.bazelrc + build --remote_cache=https://storage.googleapis.com/kibana-local-bazel-remote-cache + build --google_credentials=$BAZEL_LOCAL_DEV_CACHE_CREDENTIALS_FILE +EOF +fi + if [[ "$BAZEL_CACHE_MODE" == "buildbuddy" ]]; then echo "[bazel] enabling caching with Buildbuddy" cat <> $KIBANA_DIR/.bazelrc @@ -43,7 +52,7 @@ cat <> $KIBANA_DIR/.bazelrc EOF fi -if [[ "$BAZEL_CACHE_MODE" != @(gcs|buildbuddy|none|) ]]; then - echo "invalid value for BAZEL_CACHE_MODE received ($BAZEL_CACHE_MODE), expected one of [gcs,buildbuddy,none]" +if [[ "$BAZEL_CACHE_MODE" != @(gcs|populate-local-gcs|buildbuddy|none|) ]]; then + echo "invalid value for BAZEL_CACHE_MODE received ($BAZEL_CACHE_MODE), expected one of [gcs,populate-local-gcs|buildbuddy,none]" exit 1 fi 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/lifecycle/pre_command.sh b/.buildkite/scripts/lifecycle/pre_command.sh index 8f3776db3ca6b..11806ebf10e73 100755 --- a/.buildkite/scripts/lifecycle/pre_command.sh +++ b/.buildkite/scripts/lifecycle/pre_command.sh @@ -132,6 +132,10 @@ export SYNTHETICS_REMOTE_KIBANA_URL KIBANA_BUILDBUDDY_CI_API_KEY=$(retry 5 5 vault read -field=value secret/kibana-issues/dev/kibana-buildbuddy-ci-api-key) export KIBANA_BUILDBUDDY_CI_API_KEY +BAZEL_LOCAL_DEV_CACHE_CREDENTIALS_FILE="$HOME/.kibana-ci-bazel-remote-cache-local-dev.json" +export BAZEL_LOCAL_DEV_CACHE_CREDENTIALS_FILE +retry 5 5 vault read -field=service_account_json secret/kibana-issues/dev/kibana-ci-bazel-remote-cache-local-dev > "$BAZEL_LOCAL_DEV_CACHE_CREDENTIALS_FILE" + # By default, all steps should set up these things to get a full environment before running # It can be skipped for pipeline upload steps though, to make job start time a little faster if [[ "${SKIP_CI_SETUP:-}" != "true" ]]; then diff --git a/.buildkite/scripts/steps/artifacts/build.sh b/.buildkite/scripts/steps/artifacts/build.sh index 4519e5167c91f..337d0289daa72 100644 --- a/.buildkite/scripts/steps/artifacts/build.sh +++ b/.buildkite/scripts/steps/artifacts/build.sh @@ -24,3 +24,10 @@ buildkite-agent artifact upload "dependencies-$FULL_VERSION.csv" buildkite-agent artifact upload "dependencies-$FULL_VERSION.csv.sha512.txt" buildkite-agent artifact upload 'i18n/*.json' 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/artifacts/docker_context.sh b/.buildkite/scripts/steps/artifacts/docker_context.sh old mode 100644 new mode 100755 index a20544de18fd9..d01cbccfc76c1 --- a/.buildkite/scripts/steps/artifacts/docker_context.sh +++ b/.buildkite/scripts/steps/artifacts/docker_context.sh @@ -6,17 +6,29 @@ set -euo pipefail source .buildkite/scripts/steps/artifacts/env.sh +KIBANA_DOCKER_CONTEXT="${KIBANA_DOCKER_CONTEXT:="default"}" + echo "--- Create contexts" mkdir -p target node scripts/build --skip-initialize --skip-generic-folders --skip-platform-folders --skip-archives --docker-context-use-local-artifact $(echo "$BUILD_ARGS") -echo "--- Setup default context" +echo "--- Setup context" DOCKER_BUILD_FOLDER=$(mktemp -d) -tar -xf target/kibana-[0-9]*-docker-build-context.tar.gz -C "$DOCKER_BUILD_FOLDER" +if [[ "$KIBANA_DOCKER_CONTEXT" == "default" ]]; then + DOCKER_CONTEXT_FILE="kibana-$FULL_VERSION-docker-build-context.tar.gz" +elif [[ "$KIBANA_DOCKER_CONTEXT" == "cloud" ]]; then + DOCKER_CONTEXT_FILE="kibana-cloud-$FULL_VERSION-docker-build-context.tar.gz" +fi + +tar -xf "target/$DOCKER_CONTEXT_FILE" -C "$DOCKER_BUILD_FOLDER" cd $DOCKER_BUILD_FOLDER buildkite-agent artifact download "kibana-$FULL_VERSION-linux-x86_64.tar.gz" . --build "${KIBANA_BUILD_ID:-$BUILDKITE_BUILD_ID}" +if [[ "$KIBANA_DOCKER_CONTEXT" == "cloud" ]]; then + buildkite-agent artifact download "metricbeat-$FULL_VERSION-linux-x86_64.tar.gz" . --build "${KIBANA_BUILD_ID:-$BUILDKITE_BUILD_ID}" + buildkite-agent artifact download "filebeat-$FULL_VERSION-linux-x86_64.tar.gz" . --build "${KIBANA_BUILD_ID:-$BUILDKITE_BUILD_ID}" +fi echo "--- Build context" docker build . diff --git a/.buildkite/scripts/steps/bazel_cache/bootstrap_linux.sh b/.buildkite/scripts/steps/bazel_cache/bootstrap_linux.sh new file mode 100755 index 0000000000000..7798370bfbd35 --- /dev/null +++ b/.buildkite/scripts/steps/bazel_cache/bootstrap_linux.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +export BAZEL_CACHE_MODE=populate-local-gcs +export DISABLE_BOOTSTRAP_VALIDATION=true + +# Clear out bazel cache between runs to make sure that any artifacts that don't exist in the cache are uploaded +rm -rf ~/.bazel-cache + +.buildkite/scripts/bootstrap.sh diff --git a/.buildkite/scripts/steps/bazel_cache/bootstrap_mac.sh b/.buildkite/scripts/steps/bazel_cache/bootstrap_mac.sh index 62aabf496fd8a..ab642ec431486 100755 --- a/.buildkite/scripts/steps/bazel_cache/bootstrap_mac.sh +++ b/.buildkite/scripts/steps/bazel_cache/bootstrap_mac.sh @@ -2,13 +2,19 @@ set -euo pipefail -export BAZEL_CACHE_MODE=buildbuddy +source .buildkite/scripts/common/util.sh + +export BAZEL_CACHE_MODE=populate-local-gcs export DISABLE_BOOTSTRAP_VALIDATION=true +# Clear out bazel cache between runs to make sure that any artifacts that don't exist in the cache are uploaded +rm -rf ~/.bazel-cache + # Since our Mac agents are currently static, # use a temporary HOME directory that gets cleaned out between builds TMP_HOME="$WORKSPACE/tmp_home" rm -rf "$TMP_HOME" +mkdir -p "$TMP_HOME" export HOME="$TMP_HOME" .buildkite/scripts/bootstrap.sh 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 e98ba1d451ff3..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 @@ -499,6 +500,7 @@ /x-pack/plugins/security_solution/public/common/components/health_truncate_text @elastic/security-detections-response-rules /x-pack/plugins/security_solution/public/common/components/links_to_docs @elastic/security-detections-response-rules /x-pack/plugins/security_solution/public/common/components/callouts @elastic/security-detections-response-rules +/x-pack/plugins/security_solution/public/common/components/ml_popover @elastic/security-detections-response-rules /x-pack/plugins/security_solution/public/detections/components/callouts @elastic/security-detections-response-rules /x-pack/plugins/security_solution/public/detections/mitre @elastic/security-detections-response-rules /x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules @elastic/security-detections-response-rules 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/api/logstash-configuration-management/list-pipeline.asciidoc b/docs/api/logstash-configuration-management/list-pipeline.asciidoc index d875ea3d95b78..03f4820ac4758 100644 --- a/docs/api/logstash-configuration-management/list-pipeline.asciidoc +++ b/docs/api/logstash-configuration-management/list-pipeline.asciidoc @@ -6,6 +6,8 @@ experimental[] List all centrally-managed Logstash pipelines. +IMPORTANT: Limit the number of pipelines to 10k or fewer. As the number of pipelines nears and surpasses 10k, you may see performance issues on {kib}. + [[logstash-configuration-management-api-list-request]] ==== Request 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/docs/maps/asset-tracking-tutorial.asciidoc b/docs/maps/asset-tracking-tutorial.asciidoc index c53adf90ec3a2..46248c5280b20 100644 --- a/docs/maps/asset-tracking-tutorial.asciidoc +++ b/docs/maps/asset-tracking-tutorial.asciidoc @@ -8,7 +8,7 @@ In this tutorial, you’ll look at live urban transit data from the city of Port You’ll learn to: -- Use Logstash to ingest the TriMet REST API into Elasticsearch. +- Use {filebeat} to ingest the TriMet REST API into Elasticsearch. - Create a map with layers that visualize asset tracks and last-known locations. - Use symbols and colors to style data values and show which direction an asset is heading. - Set up tracking containment alerts to monitor moving vehicles. @@ -23,137 +23,294 @@ image::maps/images/asset-tracking-tutorial/construction_zones.png[] - If you don’t already have {kib}, set it up with https://www.elastic.co/cloud/elasticsearch-service/signup?baymax=docs-body&elektra=docs[our free trial]. Download the deployment credentials. - Obtain an API key for https://developer.trimet.org/[TriMet web services] at https://developer.trimet.org/appid/registration/. -- https://www.elastic.co/guide/en/logstash/current/getting-started-with-logstash.html[Install Logstash]. +- https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation-configuration.html[Install Filebeat]. [float] === Part 1: Ingest the Portland bus data -To get to the fun of visualizing and alerting on Portland buses, you must first create a Logstash pipeline to ingest the TriMet Portland bus data into {es}. +To get to the fun of visualizing and alerting on Portland buses, you must first create a {filebeat} input to ingest the TriMet Portland bus data into {es}. [float] ==== Step 1: Set up an Elasticsearch index . In Kibana, open the main menu, then click *Dev Tools*. -. In *Console*, create the `tri_met_tracks` index: +. In *Console*, create the `tri_met_tracks` index lifecyle policy. This policy will keep the events in the hot data phase for 7 days. The data then moves to the warm phase. After 365 days in the warm phase, the data is deleted. + [source,js] ---------------------------------- -PUT tri_met_tracks +PUT _ilm/policy/tri_met_tracks +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_primary_shard_size": "50gb", + "max_age": "7d" + }, + "set_priority": { + "priority": 100 + } + } + }, + "warm": { + "min_age": "0d", + "actions": { + "set_priority": { + "priority": 50 + } + } + }, + "delete": { + "min_age": "365d", + "actions": { + "delete": { + "delete_searchable_snapshot": true + } + } + } + } + } +} ---------------------------------- - -. To configure the `tri_met_tracks` index mappings, run: +. In *Console*, create the `tri_met_tracks` index template, which is configured to use datastreams: + [source,js] ---------------------------------- -PUT tri_met_tracks/_mapping +PUT _index_template/tri_met_tracks { - "properties": { - "in_congestion": { - "type": "boolean" + "template": { + "settings": { + "index": { + "lifecycle": { + "name": "tri_met_tracks" + } + } }, - "location": { - "type": "geo_point" + "mappings": { + "_routing": { + "required": false + }, + "numeric_detection": false, + "dynamic_date_formats": [ + "strict_date_optional_time", + "yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z" + ], + "dynamic": true, + "_source": { + "excludes": [], + "includes": [], + "enabled": true + }, + "dynamic_templates": [], + "date_detection": true, + "properties": { + "trimet": { + "type": "object", + "properties": { + "expires": { + "type": "date" + }, + "signMessage": { + "type": "text" + }, + "serviceDate": { + "type": "date" + }, + "loadPercentage": { + "type": "float" + }, + "nextStopSeq": { + "type": "integer" + }, + "source": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "blockID": { + "type": "integer" + }, + "signMessageLong": { + "type": "text" + }, + "lastLocID": { + "type": "integer" + }, + "nextLocID": { + "type": "integer" + }, + "locationInScheduleDay": { + "type": "integer" + }, + "newTrip": { + "type": "boolean" + }, + "direction": { + "type": "integer" + }, + "inCongestion": { + "type": "boolean" + }, + "routeNumber": { + "type": "integer" + }, + "bearing": { + "type": "integer" + }, + "garage": { + "type": "keyword" + }, + "tripID": { + "type": "integer" + }, + "delay": { + "type": "integer" + }, + "extraBlockID": { + "type": "integer" + }, + "messageCode": { + "type": "integer" + }, + "lastStopSeq": { + "type": "integer" + }, + "location": { + "type": "geo_point" + }, + "time": { + "index": true, + "ignore_malformed": false, + "store": false, + "type": "date", + "doc_values": true + }, + "vehicleID": { + "type": "integer" + }, + "offRoute": { + "type": "boolean" + } + } + } + } + } + }, + "index_patterns": [ + "tri_met_tracks*" + ], + "data_stream": { + "hidden": false, + "allow_custom_routing": false + }, + "composed_of": [] +} +---------------------------------- +. In **Console**, add the `tri_met_track` ingest pipeline. ++ +[source,js] +---------------------------------- +PUT _ingest/pipeline/tri_met_tracks +{ + "processors": [ + { + "set": { + "field": "trimet.inCongestion", + "value": "false", + "if": "ctx?.trimet?.inCongestion == null" + } }, - "route": { - "type": "keyword" + { + "convert": { + "field": "trimet.bearing", + "type": "float" + } }, - "time": { - "type": "date", - "format": "epoch_millis" + { + "convert": { + "field": "trimet.inCongestion", + "type": "boolean" + } }, - "type": { - "type": "keyword" + { + "script": { + "source": "ctx['trimet']['location'] = ctx['trimet']['latitude'] + \",\" + ctx['trimet']['longitude']" + } + }, + { + "script": { + "source": "ctx['_id'] = ctx['trimet']['vehicleID'] + \"_\" + ctx['trimet']['time']", + "description": "Generate documentID" + } + }, + { + "remove": { + "field": [ + "message", + "input", + "agent", + "ecs", + "host", + "event", + "trimet.longitude", + "trimet.latitude" + ] + } }, - "vehicle_id": { - "type": "keyword" + { + "set": { + "field": "_index", + "value": "tri_met_tracks" + } } - } + ] } ---------------------------------- [float] -==== Step 2: Start Logstash +==== Step 2: Start {filebeat} -. In your `logstash/config` folder, create the file `trimet-pipeline.conf`. -. Copy the pipeline script into your `trimet-pipeline.conf` file. +. Replace the contents in your `filebeat.yml` file with the following: + [source,yaml] ---------------------------------- -input { - http_poller { - urls => { - trimet => "https://developer.trimet.org/ws/v2/vehicles?appID=" - } - request_timeout => 60 - schedule => { cron => "* * * * * UTC"} - codec => "json" - } -} - -filter { - split { - field => "[resultSet][vehicle]" - } - - if ![resultSet][vehicle][inCongestion] { - mutate { - update => { - "[resultSet][vehicle][inCongestion]" => "false" - } - } - } +filebeat.inputs: +# Fetch trimet bus data every minute. +- type: httpjson + interval: 1m + request.url: "https://developer.trimet.org/ws/v2/vehicles?appID=" + response.split: + target: body.resultSet.vehicle + processors: + - decode_json_fields: + fields: ["message"] + target: "trimet" - mutate { - add_field => { - "bearing" => "%{[resultSet][vehicle][bearing]}" - "in_congestion" => "%{[resultSet][vehicle][inCongestion]}" - "location" => "%{[resultSet][vehicle][latitude]},%{[resultSet][vehicle][longitude]}" - "route" => "%{[resultSet][vehicle][routeNumber]}" - "time" => "%{[resultSet][vehicle][time]}" - "type" => "%{[resultSet][vehicle][type]}" - "vehicle_id" => "%{[resultSet][vehicle][vehicleID]}" - } - remove_field => [ "resultSet", "@version", "@timestamp", "[event][original]" ] - } + pipeline: "tri_met_tracks" - mutate { - convert => { - "bearing" => "float" - "in_congestion" => "boolean" - "time" => "integer" - } - } -} -output { - stdout { - codec => rubydebug - } +# ---------------------------- Elastic Cloud Output ---------------------------- +cloud.id: +cloud.auth: - elasticsearch { - cloud_auth => "" - cloud_id => "" - index => "tri_met_tracks" - document_id => "%{[vehicle_id]}_%{[time]}" - } -} ---------------------------------- . Replace `` with your TriMet application id. . Replace `` with your Elastic Cloud deployment credentials. . Replace `` with your {ece}/ece-cloud-id.html[elastic cloud id]. -. Open a terminal window, and then navigate to the Logstash folder. -. In your `logstash` folder, run Logstash with the TriMet pipeline: +. Open a terminal window, and then navigate to the {filebeat} folder. +. In your `filebeat` folder, run {filebeat} with the edited config: + [source,bash] ---------------------------------- -bin/logstash -f config/trimet-pipeline.conf +/bin/filebeat -c filebeat.yml ---------------------------------- -. Wait for Logstash to initialize and confirm data is flowing. You should see messages similar to this: -+ -[role="screenshot"] -image::maps/images/asset-tracking-tutorial/logstash_output.png[] -. Leave the terminal window open and Logstash running throughout this tutorial. +. Wait for {filebeat} to start shipping data to Elastic Cloud. {filebeat} should not produce any output to stdout. + +. Leave the terminal window open and {filebeat} running throughout this tutorial. [float] ==== Step 3: Create a data view for the tri_met_tracks {es} index @@ -162,13 +319,13 @@ image::maps/images/asset-tracking-tutorial/logstash_output.png[] . Click *Create data view*. . Give the data view a name: *tri_met_tracks**. . Click *Next step*. -. Set the *Time field* to *time*. +. Set the *Time field* to *trimet.time*. . Click *Create data view*. {kib} shows the fields in your data view. [role="screenshot"] -image::maps/images/asset-tracking-tutorial/index_pattern.png[] +image::maps/images/asset-tracking-tutorial/data_view.png[] [float] ==== Step 4: Explore the Portland bus data @@ -176,14 +333,14 @@ image::maps/images/asset-tracking-tutorial/index_pattern.png[] . Open the main menu, and click *Discover*. . Set the data view to *tri_met_tracks**. . Open the <>, and set the time range to the last 15 minutes. -. Expand a document and explore some of the fields that you will use later in this tutorial: `bearing`, `in_congestion`, `location`, and `vehicle_id`. +. Expand a document and explore some of the fields that you will use later in this tutorial: `trimet.bearing`, `trimet.inCongestion`, `trimet.location`, and `trimet.vehicleID`. [role="screenshot"] image::maps/images/asset-tracking-tutorial/discover.png[] [float] === Part 2: Build an operational map -It's hard to get an overview of Portland buses by looking at individual events. Let's create a map to show the bus routes and current location for each bus, along with the direction the buses are headed. +It's hard to get an overview of Portland buses by looking at individual events. Let's create a map to show the bus routes and current location for each bus, along with the direction the buses are heading. [float] ==== Step 1: Create your map @@ -204,8 +361,8 @@ Add a layer to show the bus routes for the last 15 minutes. . Click *Tracks*. . Select the *tri_met_tracks** data view. . Define the tracks: -.. Set *Entity* to *vehicle_id*. -.. Set *Sort* to *time*. +.. Set *Entity* to *trimet.vehicleID*. +.. Set *Sort* to *trimet.time*. . Click *Add layer*. . In Layer settings: .. Set *Name* to *Buses*. @@ -227,22 +384,22 @@ Add a layer that uses attributes in the data to set the style and orientation of . Click *Add layer*, and then select *Top Hits per entity*. . Select the *tri_met_tracks** data view. . To display the most recent location per bus: -.. Set *Entity* to *vehicle_id*. +.. Set *Entity* to *trimet.vehicleID*. .. Set *Documents per entity* to 1. -.. Set *Sort field* to *time*. +.. Set *Sort field* to *trimet.time*. .. Set *Sort order* to *descending*. . Click *Add layer*. . Scroll to *Layer Style*. .. Set *Symbol type* to *icon*. .. Set *Icon* to *arrow-es*. .. Set the *Fill color*: -... Select *By value* styling, and set the field to *in_congestion*. +... Select *By value* styling, and set the field to *trimet.inCongestion*. ... Use a *Custom color palette*. ... Set the *Other* color to black. ... Add a green class for *false*, meaning the bus is not in traffic. ... Add a red class for *true*, meaning the bus is in congestion. .. Set *Border width* to 0. -.. Change *Symbol orientation* to use *By value* and the *bearing* field. +.. Change *Symbol orientation* to use *By value* and the *trimet.bearing* field. + [role="screenshot"] image::maps/images/asset-tracking-tutorial/top_hits_layer_style.png[] @@ -265,7 +422,7 @@ Add a layer for construction zones, which you will draw on the map. The construc . Click *Add layer*. . Click *Create index*. -. Set *Index name* to *construction_zones*. +. Set *Index name* to *trimet_construction_zones*. . Click *Create index*. . Draw 2 or 3 construction zones on your map: .. In the toolbar on left side of the map, select the bounding box icon image:maps/images/asset-tracking-tutorial/bounding_box_icon.png[bounding box icon]. @@ -304,8 +461,8 @@ image::maps/images/asset-tracking-tutorial/rule_configuration.png[] . Select the *Tracking containment* rule type. . Set *Select entity*: .. Set *INDEX* to *tri_met_tracks**. -.. Set *BY* to *vehicle_id*. -. Set *Select boundary* *INDEX* to *construction_zones*. +.. Set *BY* to *trimet.vehicleID*. +. Set *Select boundary* *INDEX* to *trimet_construction_zones*. + [role="screenshot"] image::maps/images/asset-tracking-tutorial/tracking_containment_configuration.png[] diff --git a/docs/maps/images/asset-tracking-tutorial/data_view.png b/docs/maps/images/asset-tracking-tutorial/data_view.png new file mode 100644 index 0000000000000..36f010ca92721 Binary files /dev/null and b/docs/maps/images/asset-tracking-tutorial/data_view.png differ diff --git a/docs/maps/images/asset-tracking-tutorial/discover.png b/docs/maps/images/asset-tracking-tutorial/discover.png index d5ea0e55eedde..2f7ac73ba0939 100644 Binary files a/docs/maps/images/asset-tracking-tutorial/discover.png and b/docs/maps/images/asset-tracking-tutorial/discover.png differ diff --git a/docs/maps/images/asset-tracking-tutorial/index_pattern.png b/docs/maps/images/asset-tracking-tutorial/index_pattern.png deleted file mode 100644 index e1aaecbe62d65..0000000000000 Binary files a/docs/maps/images/asset-tracking-tutorial/index_pattern.png and /dev/null differ diff --git a/docs/maps/images/asset-tracking-tutorial/top_hits_layer_style.png b/docs/maps/images/asset-tracking-tutorial/top_hits_layer_style.png index b4a869529ad45..d77a645160bd2 100644 Binary files a/docs/maps/images/asset-tracking-tutorial/top_hits_layer_style.png and b/docs/maps/images/asset-tracking-tutorial/top_hits_layer_style.png differ diff --git a/docs/maps/images/asset-tracking-tutorial/tracking_containment_configuration.png b/docs/maps/images/asset-tracking-tutorial/tracking_containment_configuration.png index 4baf34bb414f1..87101fee884bf 100644 Binary files a/docs/maps/images/asset-tracking-tutorial/tracking_containment_configuration.png and b/docs/maps/images/asset-tracking-tutorial/tracking_containment_configuration.png differ diff --git a/docs/maps/images/inspector.png b/docs/maps/images/inspector.png deleted file mode 100644 index 0d59394caeda2..0000000000000 Binary files a/docs/maps/images/inspector.png and /dev/null differ diff --git a/docs/maps/images/requests_inspector.png b/docs/maps/images/requests_inspector.png new file mode 100644 index 0000000000000..14bde6ac7c061 Binary files /dev/null and b/docs/maps/images/requests_inspector.png differ diff --git a/docs/maps/images/vector_tile_inspector.png b/docs/maps/images/vector_tile_inspector.png new file mode 100644 index 0000000000000..94914802d540b Binary files /dev/null and b/docs/maps/images/vector_tile_inspector.png differ diff --git a/docs/maps/trouble-shooting.asciidoc b/docs/maps/trouble-shooting.asciidoc index 13c8b97c30b3d..3e4a6dfb42dc1 100644 --- a/docs/maps/trouble-shooting.asciidoc +++ b/docs/maps/trouble-shooting.asciidoc @@ -12,10 +12,13 @@ Use the information in this section to inspect Elasticsearch requests and find s [float] === Inspect Elasticsearch requests -Maps uses the {ref}/search-search.html[{es} search API] to get documents and aggregation results from {es}. To troubleshoot these requests, open the Inspector, which shows the most recent requests for each layer. You can switch between different requests using the *Request* dropdown. +Maps uses the {ref}/search-vector-tile-api.html[{es} vector tile search API] and the {ref}/search-search.html[{es} search API] to get documents and aggregation results from {es}. Use *Vector tiles* inspector to view {es} vector tile search API requests. Use *Requests* inspector to view {es} search API requests. [role="screenshot"] -image::maps/images/inspector.png[] +image::maps/images/vector_tile_inspector.png[] + +[role="screenshot"] +image::maps/images/requests_inspector.png[] [float] === Solutions to common problems diff --git a/package.json b/package.json index 577feb8f59ba4..74e4b3e211504 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "yarn": "^1.21.1" }, "resolutions": { - "**/@babel/runtime": "^7.17.8", + "**/@babel/runtime": "^7.17.9", "**/@types/node": "16.11.7", "**/chokidar": "^3.4.3", "**/deepmerge": "^4.2.2", @@ -97,7 +97,7 @@ "puppeteer/node-fetch": "^2.6.7" }, "dependencies": { - "@babel/runtime": "^7.17.8", + "@babel/runtime": "^7.17.9", "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", "@dnd-kit/utilities": "^2.0.0", @@ -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", @@ -223,7 +223,7 @@ "abort-controller": "^3.0.0", "antlr4ts": "^0.5.0-alpha.3", "archiver": "^5.2.0", - "axios": "^0.21.1", + "axios": "^0.27.2", "base64-js": "^1.3.1", "bitmap-sdf": "^1.0.3", "brace": "0.11.1", @@ -446,12 +446,12 @@ "devDependencies": { "@apidevtools/swagger-parser": "^10.0.3", "@babel/cli": "^7.17.6", - "@babel/core": "^7.17.8", + "@babel/core": "^7.17.9", "@babel/eslint-parser": "^7.17.0", "@babel/eslint-plugin": "^7.17.7", - "@babel/generator": "^7.17.7", + "@babel/generator": "^7.17.9", "@babel/helper-plugin-utils": "^7.16.7", - "@babel/parser": "^7.17.8", + "@babel/parser": "^7.17.9", "@babel/plugin-proposal-class-properties": "^7.16.7", "@babel/plugin-proposal-export-namespace-from": "^7.16.7", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", @@ -463,7 +463,7 @@ "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.16.7", "@babel/register": "^7.17.7", - "@babel/traverse": "^7.17.3", + "@babel/traverse": "^7.17.9", "@babel/types": "^7.17.0", "@bazel/ibazel": "^0.16.2", "@bazel/typescript": "4.6.2", @@ -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", @@ -796,7 +795,7 @@ "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-istanbul": "^6.1.1", "babel-plugin-require-context-hook": "^1.0.0", - "babel-plugin-styled-components": "^2.0.6", + "babel-plugin-styled-components": "^2.0.7", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "backport": "^7.3.1", "callsites": "^3.1.0", @@ -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", @@ -951,7 +949,6 @@ "url-loader": "^2.2.0", "val-loader": "^1.1.1", "vinyl-fs": "^3.0.3", - "wait-on": "^5.2.1", "watchpack": "^1.6.0", "webpack": "^4.41.5", "webpack-bundle-analyzer": "^4.5.0", diff --git a/packages/analytics/client/src/schema/types.test.ts b/packages/analytics/client/src/schema/types.test.ts index 05eccf2bb19c7..9793528c21682 100644 --- a/packages/analytics/client/src/schema/types.test.ts +++ b/packages/analytics/client/src/schema/types.test.ts @@ -182,6 +182,66 @@ describe('schema types', () => { }); }); + describe('Date value', () => { + test('it should allow the correct type and enforce the _meta.description', () => { + let valueType: SchemaValue = { + type: 'date', + _meta: { + description: 'Some description', + }, + }; + + valueType = { + type: 'keyword', + _meta: { + description: 'Some description', + optional: false, + }, + }; + + valueType = { + // @ts-expect-error because the type does not match + type: 'long', + _meta: { + description: 'Some description', + optional: false, + }, + }; + + valueType = { + type: 'keyword', + _meta: { + description: 'Some description', + // @ts-expect-error optional can't be true when the types don't set the value as optional + optional: true, + }, + }; + + // @ts-expect-error because it's missing the _meta.description + valueType = { type: 'date' }; + expect(valueType).not.toBeUndefined(); // <-- Only to stop the var-not-used complain + }); + test('it should enforce `_meta.optional: true`', () => { + let valueType: SchemaValue = { + type: 'date', + _meta: { + description: 'Some description', + optional: true, + }, + }; + + valueType = { + type: 'date', + _meta: { + description: 'Some description', + // @ts-expect-error because optional can't be false when the value can be undefined + optional: false, + }, + }; + expect(valueType).not.toBeUndefined(); // <-- Only to stop the var-not-used complain + }); + }); + describe('Object value', () => { test('it should allow "pass_through" and enforce the _meta.description', () => { let valueType: SchemaValue<{ a_value: string }> = { diff --git a/packages/analytics/client/src/schema/types.ts b/packages/analytics/client/src/schema/types.ts index 5043c46e73fd4..35a035bf47b21 100644 --- a/packages/analytics/client/src/schema/types.ts +++ b/packages/analytics/client/src/schema/types.ts @@ -31,7 +31,7 @@ export type AllowedSchemaTypes = /** * Helper to ensure the declared types match the schema types */ -export type PossibleSchemaTypes = Value extends string +export type PossibleSchemaTypes = Value extends string | Date ? AllowedSchemaStringTypes : Value extends number ? AllowedSchemaNumberTypes @@ -66,6 +66,8 @@ export type SchemaValue = : // Otherwise, try to infer the type and enforce the schema NonNullable extends Array | ReadonlyArray ? SchemaArray + : NonNullable extends Date + ? SchemaChildValue : NonNullable extends object ? SchemaObject : SchemaChildValue); diff --git a/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.test.ts b/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.test.ts index 6fbe8fc166586..fae3121372193 100644 --- a/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.test.ts +++ b/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.test.ts @@ -130,7 +130,7 @@ describe('ElasticV3BrowserShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -171,7 +171,7 @@ describe('ElasticV3BrowserShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -206,7 +206,7 @@ describe('ElasticV3BrowserShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -230,7 +230,7 @@ describe('ElasticV3BrowserShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -268,7 +268,7 @@ describe('ElasticV3BrowserShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, diff --git a/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.ts b/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.ts index 5607d3d79a13d..780d3cd64a5af 100644 --- a/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.ts +++ b/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.ts @@ -97,7 +97,7 @@ export class ElasticV3BrowserShipper implements IShipper { } private async makeRequest(events: Event[]): Promise { - const { status, text, ok } = await fetch(this.url, { + const response = await fetch(this.url, { method: 'POST', body: eventsToNDJSON(events), headers: buildHeaders(this.clusterUuid, this.options.version, this.licenseId), @@ -108,14 +108,17 @@ export class ElasticV3BrowserShipper implements IShipper { if (this.options.debug) { this.initContext.logger.debug( - `[${ElasticV3BrowserShipper.shipperName}]: ${status} - ${await text()}` + `[${ElasticV3BrowserShipper.shipperName}]: ${response.status} - ${await response.text()}` ); } - if (!ok) { - throw new ErrorWithCode(`${status} - ${await text()}`, `${status}`); + if (!response.ok) { + throw new ErrorWithCode( + `${response.status} - ${await response.text()}`, + `${response.status}` + ); } - return `${status}`; + return `${response.status}`; } } diff --git a/packages/analytics/shippers/elastic_v3/common/src/build_headers.test.ts b/packages/analytics/shippers/elastic_v3/common/src/build_headers.test.ts index 468824916ec48..ecc350006eef9 100644 --- a/packages/analytics/shippers/elastic_v3/common/src/build_headers.test.ts +++ b/packages/analytics/shippers/elastic_v3/common/src/build_headers.test.ts @@ -12,7 +12,7 @@ describe('buildHeaders', () => { test('builds the headers as expected in the V3 endpoints', () => { expect(buildHeaders('test-cluster', '1.2.3', 'test-license')).toMatchInlineSnapshot(` Object { - "content-type": "application/x-njson", + "content-type": "application/x-ndjson", "x-elastic-cluster-id": "test-cluster", "x-elastic-license-id": "test-license", "x-elastic-stack-version": "1.2.3", @@ -23,7 +23,7 @@ describe('buildHeaders', () => { test('if license is not provided, it skips the license header', () => { expect(buildHeaders('test-cluster', '1.2.3')).toMatchInlineSnapshot(` Object { - "content-type": "application/x-njson", + "content-type": "application/x-ndjson", "x-elastic-cluster-id": "test-cluster", "x-elastic-stack-version": "1.2.3", } diff --git a/packages/analytics/shippers/elastic_v3/common/src/build_headers.ts b/packages/analytics/shippers/elastic_v3/common/src/build_headers.ts index 43126cf9d5629..6f4238b41d8e4 100644 --- a/packages/analytics/shippers/elastic_v3/common/src/build_headers.ts +++ b/packages/analytics/shippers/elastic_v3/common/src/build_headers.ts @@ -8,7 +8,7 @@ export function buildHeaders(clusterUuid: string, version: string, licenseId?: string) { return { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': clusterUuid, 'x-elastic-stack-version': version, ...(licenseId && { 'x-elastic-license-id': licenseId }), diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts index ffdfd797437a9..d7e8ee379e528 100644 --- a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts @@ -129,7 +129,7 @@ describe('ElasticV3ServerShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -171,7 +171,7 @@ describe('ElasticV3ServerShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -208,7 +208,7 @@ describe('ElasticV3ServerShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -241,7 +241,7 @@ describe('ElasticV3ServerShipper', () => { ) .join(''), headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -284,7 +284,7 @@ describe('ElasticV3ServerShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -322,7 +322,7 @@ describe('ElasticV3ServerShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, @@ -357,7 +357,7 @@ describe('ElasticV3ServerShipper', () => { { body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', headers: { - 'content-type': 'application/x-njson', + 'content-type': 'application/x-ndjson', 'x-elastic-cluster-id': 'UNKNOWN', 'x-elastic-stack-version': '1.2.3', }, diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts index 1c1277f7d7b4b..dca510d8fe5fa 100644 --- a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts @@ -258,17 +258,21 @@ export class ElasticV3ServerShipper implements IShipper { } private async sendEvents(events: Event[]) { + this.initContext.logger.debug(`Reporting ${events.length} events...`); try { const code = await this.makeRequest(events); this.reportTelemetryCounters(events, { code }); + this.initContext.logger.debug(`Reported ${events.length} events...`); } catch (error) { + this.initContext.logger.debug(`Failed to report ${events.length} events...`); + this.initContext.logger.debug(error); this.reportTelemetryCounters(events, { code: error.code, error }); this.firstTimeOffline = undefined; } } private async makeRequest(events: Event[]): Promise { - const { status, text, ok } = await fetch(this.url, { + const response = await fetch(this.url, { method: 'POST', body: eventsToNDJSON(events), headers: buildHeaders(this.clusterUuid, this.options.version, this.licenseId), @@ -276,15 +280,16 @@ export class ElasticV3ServerShipper implements IShipper { }); if (this.options.debug) { - this.initContext.logger.debug( - `[${ElasticV3ServerShipper.shipperName}]: ${status} - ${await text()}` - ); + this.initContext.logger.debug(`${response.status} - ${await response.text()}`); } - if (!ok) { - throw new ErrorWithCode(`${status} - ${await text()}`, `${status}`); + if (!response.ok) { + throw new ErrorWithCode( + `${response.status} - ${await response.text()}`, + `${response.status}` + ); } - return `${status}`; + return `${response.status}`; } } diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 818825096ffc1..9c59db0f47f2b 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`, @@ -443,6 +444,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { guide: `${KIBANA_DOCS}maps.html`, importGeospatialPrivileges: `${KIBANA_DOCS}import-geospatial-data.html#import-geospatial-privileges`, gdalTutorial: `${ELASTIC_WEBSITE_URL}blog/how-to-ingest-geospatial-data-into-elasticsearch-with-gdal`, + termJoinsExample: `${KIBANA_DOCS}terms-join.html#_example_term_join`, }, monitoring: { alertsKibana: `${KIBANA_DOCS}kibana-alerts.html`, @@ -526,6 +528,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..ce6533b93f9e3 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; @@ -309,6 +310,7 @@ export interface DocLinks { guide: string; importGeospatialPrivileges: string; gdalTutorial: string; + termJoinsExample: string; }>; readonly monitoring: Record; readonly reporting: Readonly<{ @@ -337,6 +339,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..561007cb33b23 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 @@ -57,7 +58,7 @@ pageLoadAssetSize: telemetry: 51957 telemetryManagementSection: 38586 transform: 41007 - triggersActionsUi: 104400 + triggersActionsUi: 105800 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728 upgradeAssistant: 81241 urlForwarding: 32579 usageCollection: 39762 @@ -127,5 +128,5 @@ pageLoadAssetSize: eventAnnotation: 19334 screenshotting: 22870 synthetics: 40958 - expressionXY: 30000 + expressionXY: 31000 kibanaUsageCollection: 16463 diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index f2d5a60cd325e..5045611c2ac2c 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -15289,6 +15289,447 @@ const arrify = value => { module.exports = arrify; +/***/ }), + +/***/ "../../node_modules/asynckit/index.js": +/***/ (function(module, exports, __webpack_require__) { + +module.exports = +{ + parallel : __webpack_require__("../../node_modules/asynckit/parallel.js"), + serial : __webpack_require__("../../node_modules/asynckit/serial.js"), + serialOrdered : __webpack_require__("../../node_modules/asynckit/serialOrdered.js") +}; + + +/***/ }), + +/***/ "../../node_modules/asynckit/lib/abort.js": +/***/ (function(module, exports) { + +// API +module.exports = abort; + +/** + * Aborts leftover active jobs + * + * @param {object} state - current state object + */ +function abort(state) +{ + Object.keys(state.jobs).forEach(clean.bind(state)); + + // reset leftover jobs + state.jobs = {}; +} + +/** + * Cleans up leftover job by invoking abort function for the provided job id + * + * @this state + * @param {string|number} key - job id to abort + */ +function clean(key) +{ + if (typeof this.jobs[key] == 'function') + { + this.jobs[key](); + } +} + + +/***/ }), + +/***/ "../../node_modules/asynckit/lib/async.js": +/***/ (function(module, exports, __webpack_require__) { + +var defer = __webpack_require__("../../node_modules/asynckit/lib/defer.js"); + +// API +module.exports = async; + +/** + * Runs provided callback asynchronously + * even if callback itself is not + * + * @param {function} callback - callback to invoke + * @returns {function} - augmented callback + */ +function async(callback) +{ + var isAsync = false; + + // check if async happened + defer(function() { isAsync = true; }); + + return function async_callback(err, result) + { + if (isAsync) + { + callback(err, result); + } + else + { + defer(function nextTick_callback() + { + callback(err, result); + }); + } + }; +} + + +/***/ }), + +/***/ "../../node_modules/asynckit/lib/defer.js": +/***/ (function(module, exports) { + +module.exports = defer; + +/** + * Runs provided function on next iteration of the event loop + * + * @param {function} fn - function to run + */ +function defer(fn) +{ + var nextTick = typeof setImmediate == 'function' + ? setImmediate + : ( + typeof process == 'object' && typeof process.nextTick == 'function' + ? process.nextTick + : null + ); + + if (nextTick) + { + nextTick(fn); + } + else + { + setTimeout(fn, 0); + } +} + + +/***/ }), + +/***/ "../../node_modules/asynckit/lib/iterate.js": +/***/ (function(module, exports, __webpack_require__) { + +var async = __webpack_require__("../../node_modules/asynckit/lib/async.js") + , abort = __webpack_require__("../../node_modules/asynckit/lib/abort.js") + ; + +// API +module.exports = iterate; + +/** + * Iterates over each job object + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {object} state - current job status + * @param {function} callback - invoked when all elements processed + */ +function iterate(list, iterator, state, callback) +{ + // store current index + var key = state['keyedList'] ? state['keyedList'][state.index] : state.index; + + state.jobs[key] = runJob(iterator, key, list[key], function(error, output) + { + // don't repeat yourself + // skip secondary callbacks + if (!(key in state.jobs)) + { + return; + } + + // clean up jobs + delete state.jobs[key]; + + if (error) + { + // don't process rest of the results + // stop still active jobs + // and reset the list + abort(state); + } + else + { + state.results[key] = output; + } + + // return salvaged results + callback(error, state.results); + }); +} + +/** + * Runs iterator over provided job element + * + * @param {function} iterator - iterator to invoke + * @param {string|number} key - key/index of the element in the list of jobs + * @param {mixed} item - job description + * @param {function} callback - invoked after iterator is done with the job + * @returns {function|mixed} - job abort function or something else + */ +function runJob(iterator, key, item, callback) +{ + var aborter; + + // allow shortcut if iterator expects only two arguments + if (iterator.length == 2) + { + aborter = iterator(item, async(callback)); + } + // otherwise go with full three arguments + else + { + aborter = iterator(item, key, async(callback)); + } + + return aborter; +} + + +/***/ }), + +/***/ "../../node_modules/asynckit/lib/state.js": +/***/ (function(module, exports) { + +// API +module.exports = state; + +/** + * Creates initial state object + * for iteration over list + * + * @param {array|object} list - list to iterate over + * @param {function|null} sortMethod - function to use for keys sort, + * or `null` to keep them as is + * @returns {object} - initial state object + */ +function state(list, sortMethod) +{ + var isNamedList = !Array.isArray(list) + , initState = + { + index : 0, + keyedList: isNamedList || sortMethod ? Object.keys(list) : null, + jobs : {}, + results : isNamedList ? {} : [], + size : isNamedList ? Object.keys(list).length : list.length + } + ; + + if (sortMethod) + { + // sort array keys based on it's values + // sort object's keys just on own merit + initState.keyedList.sort(isNamedList ? sortMethod : function(a, b) + { + return sortMethod(list[a], list[b]); + }); + } + + return initState; +} + + +/***/ }), + +/***/ "../../node_modules/asynckit/lib/terminator.js": +/***/ (function(module, exports, __webpack_require__) { + +var abort = __webpack_require__("../../node_modules/asynckit/lib/abort.js") + , async = __webpack_require__("../../node_modules/asynckit/lib/async.js") + ; + +// API +module.exports = terminator; + +/** + * Terminates jobs in the attached state context + * + * @this AsyncKitState# + * @param {function} callback - final callback to invoke after termination + */ +function terminator(callback) +{ + if (!Object.keys(this.jobs).length) + { + return; + } + + // fast forward iteration index + this.index = this.size; + + // abort jobs + abort(this); + + // send back results we have so far + async(callback)(null, this.results); +} + + +/***/ }), + +/***/ "../../node_modules/asynckit/parallel.js": +/***/ (function(module, exports, __webpack_require__) { + +var iterate = __webpack_require__("../../node_modules/asynckit/lib/iterate.js") + , initState = __webpack_require__("../../node_modules/asynckit/lib/state.js") + , terminator = __webpack_require__("../../node_modules/asynckit/lib/terminator.js") + ; + +// Public API +module.exports = parallel; + +/** + * Runs iterator over provided array elements in parallel + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function parallel(list, iterator, callback) +{ + var state = initState(list); + + while (state.index < (state['keyedList'] || list).length) + { + iterate(list, iterator, state, function(error, result) + { + if (error) + { + callback(error, result); + return; + } + + // looks like it's the last one + if (Object.keys(state.jobs).length === 0) + { + callback(null, state.results); + return; + } + }); + + state.index++; + } + + return terminator.bind(state, callback); +} + + +/***/ }), + +/***/ "../../node_modules/asynckit/serial.js": +/***/ (function(module, exports, __webpack_require__) { + +var serialOrdered = __webpack_require__("../../node_modules/asynckit/serialOrdered.js"); + +// Public API +module.exports = serial; + +/** + * Runs iterator over provided array elements in series + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function serial(list, iterator, callback) +{ + return serialOrdered(list, iterator, null, callback); +} + + +/***/ }), + +/***/ "../../node_modules/asynckit/serialOrdered.js": +/***/ (function(module, exports, __webpack_require__) { + +var iterate = __webpack_require__("../../node_modules/asynckit/lib/iterate.js") + , initState = __webpack_require__("../../node_modules/asynckit/lib/state.js") + , terminator = __webpack_require__("../../node_modules/asynckit/lib/terminator.js") + ; + +// Public API +module.exports = serialOrdered; +// sorting helpers +module.exports.ascending = ascending; +module.exports.descending = descending; + +/** + * Runs iterator over provided sorted array elements in series + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} sortMethod - custom sort function + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function serialOrdered(list, iterator, sortMethod, callback) +{ + var state = initState(list, sortMethod); + + iterate(list, iterator, state, function iteratorHandler(error, result) + { + if (error) + { + callback(error, result); + return; + } + + state.index++; + + // are we there yet? + if (state.index < (state['keyedList'] || list).length) + { + iterate(list, iterator, state, iteratorHandler); + return; + } + + // done here + callback(null, state.results); + }); + + return terminator.bind(state, callback); +} + +/* + * -- Sort methods + */ + +/** + * sort helper to sort array elements in ascending order + * + * @param {mixed} a - an item to compare + * @param {mixed} b - an item to compare + * @returns {number} - comparison result + */ +function ascending(a, b) +{ + return a < b ? -1 : a > b ? 1 : 0; +} + +/** + * sort helper to sort array elements in descending order + * + * @param {mixed} a - an item to compare + * @param {mixed} b - an item to compare + * @returns {number} - comparison result + */ +function descending(a, b) +{ + return -1 * ascending(a, b); +} + + /***/ }), /***/ "../../node_modules/axios/index.js": @@ -15314,12 +15755,15 @@ var httpFollow = __webpack_require__("../../node_modules/follow-redirects/index. var httpsFollow = __webpack_require__("../../node_modules/follow-redirects/index.js").https; var url = __webpack_require__("url"); var zlib = __webpack_require__("zlib"); -var pkg = __webpack_require__("../../node_modules/axios/package.json"); -var createError = __webpack_require__("../../node_modules/axios/lib/core/createError.js"); -var enhanceError = __webpack_require__("../../node_modules/axios/lib/core/enhanceError.js"); +var VERSION = __webpack_require__("../../node_modules/axios/lib/env/data.js").version; +var transitionalDefaults = __webpack_require__("../../node_modules/axios/lib/defaults/transitional.js"); +var AxiosError = __webpack_require__("../../node_modules/axios/lib/core/AxiosError.js"); +var CanceledError = __webpack_require__("../../node_modules/axios/lib/cancel/CanceledError.js"); var isHttps = /https:?/; +var supportedProtocols = [ 'http:', 'https:', 'file:' ]; + /** * * @param {http.ClientRequestArgs} options @@ -15348,23 +15792,51 @@ function setProxy(options, proxy, location) { /*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { + var onCanceled; + function done() { + if (config.cancelToken) { + config.cancelToken.unsubscribe(onCanceled); + } + + if (config.signal) { + config.signal.removeEventListener('abort', onCanceled); + } + } var resolve = function resolve(value) { + done(); resolvePromise(value); }; + var rejected = false; var reject = function reject(value) { + done(); + rejected = true; rejectPromise(value); }; var data = config.data; var headers = config.headers; + var headerNames = {}; + + Object.keys(headers).forEach(function storeLowerName(name) { + headerNames[name.toLowerCase()] = name; + }); // Set User-Agent (required by some servers) - // Only set header if it hasn't been set in config // See https://github.com/axios/axios/issues/69 - if (!headers['User-Agent'] && !headers['user-agent']) { - headers['User-Agent'] = 'axios/' + pkg.version; + if ('user-agent' in headerNames) { + // User-Agent is specified; handle case where no UA header is desired + if (!headers[headerNames['user-agent']]) { + delete headers[headerNames['user-agent']]; + } + // Otherwise, use specified value + } else { + // Only set header if it hasn't been set in config + headers['User-Agent'] = 'axios/' + VERSION; } - if (data && !utils.isStream(data)) { + // support for https://www.npmjs.com/package/form-data api + if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { + Object.assign(headers, data.getHeaders()); + } else if (data && !utils.isStream(data)) { if (Buffer.isBuffer(data)) { // Nothing to do... } else if (utils.isArrayBuffer(data)) { @@ -15372,14 +15844,25 @@ module.exports = function httpAdapter(config) { } else if (utils.isString(data)) { data = Buffer.from(data, 'utf-8'); } else { - return reject(createError( + return reject(new AxiosError( 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', + AxiosError.ERR_BAD_REQUEST, + config + )); + } + + if (config.maxBodyLength > -1 && data.length > config.maxBodyLength) { + return reject(new AxiosError( + 'Request body larger than maxBodyLength limit', + AxiosError.ERR_BAD_REQUEST, config )); } // Add Content-Length header if data exists - headers['Content-Length'] = data.length; + if (!headerNames['content-length']) { + headers['Content-Length'] = data.length; + } } // HTTP basic authentication @@ -15393,7 +15876,15 @@ module.exports = function httpAdapter(config) { // Parse url var fullPath = buildFullPath(config.baseURL, config.url); var parsed = url.parse(fullPath); - var protocol = parsed.protocol || 'http:'; + var protocol = parsed.protocol || supportedProtocols[0]; + + if (supportedProtocols.indexOf(protocol) === -1) { + return reject(new AxiosError( + 'Unsupported protocol ' + protocol, + AxiosError.ERR_BAD_REQUEST, + config + )); + } if (!auth && parsed.auth) { var urlAuth = parsed.auth.split(':'); @@ -15402,13 +15893,23 @@ module.exports = function httpAdapter(config) { auth = urlUsername + ':' + urlPassword; } - if (auth) { - delete headers.Authorization; + if (auth && headerNames.authorization) { + delete headers[headerNames.authorization]; } var isHttpsRequest = isHttps.test(protocol); var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; + try { + buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''); + } catch (err) { + var customErr = new Error(err.message); + customErr.config = config; + customErr.url = config.url; + customErr.exists = true; + reject(customErr); + } + var options = { path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), method: config.method.toUpperCase(), @@ -15488,6 +15989,9 @@ module.exports = function httpAdapter(config) { if (config.maxRedirects) { options.maxRedirects = config.maxRedirects; } + if (config.beforeRedirect) { + options.beforeRedirect = config.beforeRedirect; + } transport = isHttpsProxy ? httpsFollow : httpFollow; } @@ -15495,6 +15999,10 @@ module.exports = function httpAdapter(config) { options.maxBodyLength = config.maxBodyLength; } + if (config.insecureHTTPParser) { + options.insecureHTTPParser = config.insecureHTTPParser; + } + // Create the request var req = transport.request(options, function handleResponse(res) { if (req.aborted) return; @@ -15535,32 +16043,52 @@ module.exports = function httpAdapter(config) { settle(resolve, reject, response); } else { var responseBuffer = []; + var totalResponseBytes = 0; stream.on('data', function handleStreamData(chunk) { responseBuffer.push(chunk); + totalResponseBytes += chunk.length; // make sure the content length is not over the maxContentLength if specified - if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { + if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) { + // stream.destoy() emit aborted event before calling reject() on Node.js v16 + rejected = true; stream.destroy(); - reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', - config, null, lastRequest)); + reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded', + AxiosError.ERR_BAD_RESPONSE, config, lastRequest)); } }); + stream.on('aborted', function handlerStreamAborted() { + if (rejected) { + return; + } + stream.destroy(); + reject(new AxiosError( + 'maxContentLength size of ' + config.maxContentLength + ' exceeded', + AxiosError.ERR_BAD_RESPONSE, + config, + lastRequest + )); + }); + stream.on('error', function handleStreamError(err) { if (req.aborted) return; - reject(enhanceError(err, config, null, lastRequest)); + reject(AxiosError.from(err, null, config, lastRequest)); }); stream.on('end', function handleStreamEnd() { - var responseData = Buffer.concat(responseBuffer); - if (config.responseType !== 'arraybuffer') { - responseData = responseData.toString(config.responseEncoding); - if (!config.responseEncoding || config.responseEncoding === 'utf8') { - responseData = utils.stripBOM(responseData); + try { + var responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer); + if (config.responseType !== 'arraybuffer') { + responseData = responseData.toString(config.responseEncoding); + if (!config.responseEncoding || config.responseEncoding === 'utf8') { + responseData = utils.stripBOM(responseData); + } } + response.data = responseData; + } catch (err) { + reject(AxiosError.from(err, null, config, response.request, response)); } - - response.data = responseData; settle(resolve, reject, response); }); } @@ -15568,37 +16096,71 @@ module.exports = function httpAdapter(config) { // Handle errors req.on('error', function handleRequestError(err) { - if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') return; - reject(enhanceError(err, config, null, req)); + // @todo remove + // if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return; + reject(AxiosError.from(err, null, config, req)); + }); + + // set tcp keep alive to prevent drop connection by peer + req.on('socket', function handleRequestSocket(socket) { + // default interval of sending ack packet is 1 minute + socket.setKeepAlive(true, 1000 * 60); }); // Handle request timeout if (config.timeout) { + // This is forcing a int timeout to avoid problems if the `req` interface doesn't handle other types. + var timeout = parseInt(config.timeout, 10); + + if (isNaN(timeout)) { + reject(new AxiosError( + 'error trying to parse `config.timeout` to int', + AxiosError.ERR_BAD_OPTION_VALUE, + config, + req + )); + + return; + } + // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system. // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET. // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up. // And then these socket which be hang up will devoring CPU little by little. // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect. - req.setTimeout(config.timeout, function handleRequestTimeout() { + req.setTimeout(timeout, function handleRequestTimeout() { req.abort(); - reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req)); + var transitional = config.transitional || transitionalDefaults; + reject(new AxiosError( + 'timeout of ' + timeout + 'ms exceeded', + transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, + config, + req + )); }); } - if (config.cancelToken) { + if (config.cancelToken || config.signal) { // Handle cancellation - config.cancelToken.promise.then(function onCanceled(cancel) { + // eslint-disable-next-line func-names + onCanceled = function(cancel) { if (req.aborted) return; req.abort(); - reject(cancel); - }); + reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel); + }; + + config.cancelToken && config.cancelToken.subscribe(onCanceled); + if (config.signal) { + config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled); + } } + // Send the request if (utils.isStream(data)) { data.on('error', function handleStreamError(err) { - reject(enhanceError(err, config, null, req)); + reject(AxiosError.from(err, config, null, req)); }).pipe(req); } else { req.end(data); @@ -15622,14 +16184,28 @@ var buildURL = __webpack_require__("../../node_modules/axios/lib/helpers/buildUR var buildFullPath = __webpack_require__("../../node_modules/axios/lib/core/buildFullPath.js"); var parseHeaders = __webpack_require__("../../node_modules/axios/lib/helpers/parseHeaders.js"); var isURLSameOrigin = __webpack_require__("../../node_modules/axios/lib/helpers/isURLSameOrigin.js"); -var createError = __webpack_require__("../../node_modules/axios/lib/core/createError.js"); +var transitionalDefaults = __webpack_require__("../../node_modules/axios/lib/defaults/transitional.js"); +var AxiosError = __webpack_require__("../../node_modules/axios/lib/core/AxiosError.js"); +var CanceledError = __webpack_require__("../../node_modules/axios/lib/cancel/CanceledError.js"); +var parseProtocol = __webpack_require__("../../node_modules/axios/lib/helpers/parseProtocol.js"); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { var requestData = config.data; var requestHeaders = config.headers; + var responseType = config.responseType; + var onCanceled; + function done() { + if (config.cancelToken) { + config.cancelToken.unsubscribe(onCanceled); + } - if (utils.isFormData(requestData)) { + if (config.signal) { + config.signal.removeEventListener('abort', onCanceled); + } + } + + if (utils.isFormData(requestData) && utils.isStandardBrowserEnv()) { delete requestHeaders['Content-Type']; // Let the browser set it } @@ -15643,28 +16219,20 @@ module.exports = function xhrAdapter(config) { } var fullPath = buildFullPath(config.baseURL, config.url); + request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); // Set the request timeout in MS request.timeout = config.timeout; - // Listen for ready state - request.onreadystatechange = function handleLoad() { - if (!request || request.readyState !== 4) { - return; - } - - // The request errored out and we didn't get a response, this will be - // handled by onerror instead - // With one exception: request that using file: protocol, most browsers - // will return status as 0 even though it's a successful request - if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { + function onloadend() { + if (!request) { return; } - // Prepare the response var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; - var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; + var responseData = !responseType || responseType === 'text' || responseType === 'json' ? + request.responseText : request.response; var response = { data: responseData, status: request.status, @@ -15674,11 +16242,40 @@ module.exports = function xhrAdapter(config) { request: request }; - settle(resolve, reject, response); + settle(function _resolve(value) { + resolve(value); + done(); + }, function _reject(err) { + reject(err); + done(); + }, response); // Clean up request request = null; - }; + } + + if ('onloadend' in request) { + // Use onloadend if available + request.onloadend = onloadend; + } else { + // Listen for ready state to emulate onloadend + request.onreadystatechange = function handleLoad() { + if (!request || request.readyState !== 4) { + return; + } + + // The request errored out and we didn't get a response, this will be + // handled by onerror instead + // With one exception: request that using file: protocol, most browsers + // will return status as 0 even though it's a successful request + if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { + return; + } + // readystate handler is calling before onerror or ontimeout handlers, + // so we should call onloadend on the next 'tick' + setTimeout(onloadend); + }; + } // Handle browser request cancellation (as opposed to a manual cancellation) request.onabort = function handleAbort() { @@ -15686,7 +16283,7 @@ module.exports = function xhrAdapter(config) { return; } - reject(createError('Request aborted', config, 'ECONNABORTED', request)); + reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request)); // Clean up request request = null; @@ -15696,7 +16293,7 @@ module.exports = function xhrAdapter(config) { request.onerror = function handleError() { // Real errors are hidden from us by the browser // onerror should only fire if it's a network error - reject(createError('Network Error', config, null, request)); + reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request, request)); // Clean up request request = null; @@ -15704,11 +16301,15 @@ module.exports = function xhrAdapter(config) { // Handle timeout request.ontimeout = function handleTimeout() { - var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded'; + var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded'; + var transitional = config.transitional || transitionalDefaults; if (config.timeoutErrorMessage) { timeoutErrorMessage = config.timeoutErrorMessage; } - reject(createError(timeoutErrorMessage, config, 'ECONNABORTED', + reject(new AxiosError( + timeoutErrorMessage, + transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, + config, request)); // Clean up request @@ -15748,16 +16349,8 @@ module.exports = function xhrAdapter(config) { } // Add responseType to request if needed - if (config.responseType) { - try { - request.responseType = config.responseType; - } catch (e) { - // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2. - // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function. - if (config.responseType !== 'json') { - throw e; - } - } + if (responseType && responseType !== 'json') { + request.responseType = config.responseType; } // Handle progress if needed @@ -15770,24 +16363,36 @@ module.exports = function xhrAdapter(config) { request.upload.addEventListener('progress', config.onUploadProgress); } - if (config.cancelToken) { + if (config.cancelToken || config.signal) { // Handle cancellation - config.cancelToken.promise.then(function onCanceled(cancel) { + // eslint-disable-next-line func-names + onCanceled = function(cancel) { if (!request) { return; } - + reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel); request.abort(); - reject(cancel); - // Clean up request request = null; - }); + }; + + config.cancelToken && config.cancelToken.subscribe(onCanceled); + if (config.signal) { + config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled); + } } if (!requestData) { requestData = null; } + var protocol = parseProtocol(fullPath); + + if (protocol && [ 'http', 'https', 'file' ].indexOf(protocol) === -1) { + reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config)); + return; + } + + // Send the request request.send(requestData); }); @@ -15806,7 +16411,7 @@ var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); var bind = __webpack_require__("../../node_modules/axios/lib/helpers/bind.js"); var Axios = __webpack_require__("../../node_modules/axios/lib/core/Axios.js"); var mergeConfig = __webpack_require__("../../node_modules/axios/lib/core/mergeConfig.js"); -var defaults = __webpack_require__("../../node_modules/axios/lib/defaults.js"); +var defaults = __webpack_require__("../../node_modules/axios/lib/defaults/index.js"); /** * Create an instance of Axios @@ -15824,6 +16429,11 @@ function createInstance(defaultConfig) { // Copy context to instance utils.extend(instance, context); + // Factory for creating new instances + instance.create = function create(instanceConfig) { + return createInstance(mergeConfig(defaultConfig, instanceConfig)); + }; + return instance; } @@ -15833,15 +16443,18 @@ var axios = createInstance(defaults); // Expose Axios class to allow class inheritance axios.Axios = Axios; -// Factory for creating new instances -axios.create = function create(instanceConfig) { - return createInstance(mergeConfig(axios.defaults, instanceConfig)); -}; - // Expose Cancel & CancelToken -axios.Cancel = __webpack_require__("../../node_modules/axios/lib/cancel/Cancel.js"); +axios.CanceledError = __webpack_require__("../../node_modules/axios/lib/cancel/CanceledError.js"); axios.CancelToken = __webpack_require__("../../node_modules/axios/lib/cancel/CancelToken.js"); axios.isCancel = __webpack_require__("../../node_modules/axios/lib/cancel/isCancel.js"); +axios.VERSION = __webpack_require__("../../node_modules/axios/lib/env/data.js").version; +axios.toFormData = __webpack_require__("../../node_modules/axios/lib/helpers/toFormData.js"); + +// Expose AxiosError class +axios.AxiosError = __webpack_require__("../../node_modules/axios/lib/core/AxiosError.js"); + +// alias for CanceledError for backward compatibility +axios.Cancel = axios.CanceledError; // Expose all/spread axios.all = function all(promises) { @@ -15858,33 +16471,6 @@ module.exports = axios; module.exports.default = axios; -/***/ }), - -/***/ "../../node_modules/axios/lib/cancel/Cancel.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -/** - * A `Cancel` is an object that is thrown when an operation is canceled. - * - * @class - * @param {string=} message The message. - */ -function Cancel(message) { - this.message = message; -} - -Cancel.prototype.toString = function toString() { - return 'Cancel' + (this.message ? ': ' + this.message : ''); -}; - -Cancel.prototype.__CANCEL__ = true; - -module.exports = Cancel; - - /***/ }), /***/ "../../node_modules/axios/lib/cancel/CancelToken.js": @@ -15893,7 +16479,7 @@ module.exports = Cancel; "use strict"; -var Cancel = __webpack_require__("../../node_modules/axios/lib/cancel/Cancel.js"); +var CanceledError = __webpack_require__("../../node_modules/axios/lib/cancel/CanceledError.js"); /** * A `CancelToken` is an object that can be used to request cancellation of an operation. @@ -15907,24 +16493,55 @@ function CancelToken(executor) { } var resolvePromise; + this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); var token = this; + + // eslint-disable-next-line func-names + this.promise.then(function(cancel) { + if (!token._listeners) return; + + var i; + var l = token._listeners.length; + + for (i = 0; i < l; i++) { + token._listeners[i](cancel); + } + token._listeners = null; + }); + + // eslint-disable-next-line func-names + this.promise.then = function(onfulfilled) { + var _resolve; + // eslint-disable-next-line func-names + var promise = new Promise(function(resolve) { + token.subscribe(resolve); + _resolve = resolve; + }).then(onfulfilled); + + promise.cancel = function reject() { + token.unsubscribe(_resolve); + }; + + return promise; + }; + executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; } - token.reason = new Cancel(message); + token.reason = new CanceledError(message); resolvePromise(token.reason); }); } /** - * Throws a `Cancel` if cancellation has been requested. + * Throws a `CanceledError` if cancellation has been requested. */ CancelToken.prototype.throwIfRequested = function throwIfRequested() { if (this.reason) { @@ -15932,6 +16549,37 @@ CancelToken.prototype.throwIfRequested = function throwIfRequested() { } }; +/** + * Subscribe to the cancel signal + */ + +CancelToken.prototype.subscribe = function subscribe(listener) { + if (this.reason) { + listener(this.reason); + return; + } + + if (this._listeners) { + this._listeners.push(listener); + } else { + this._listeners = [listener]; + } +}; + +/** + * Unsubscribe from the cancel signal + */ + +CancelToken.prototype.unsubscribe = function unsubscribe(listener) { + if (!this._listeners) { + return; + } + var index = this._listeners.indexOf(listener); + if (index !== -1) { + this._listeners.splice(index, 1); + } +}; + /** * Returns an object that contains a new `CancelToken` and a function that, when called, * cancels the `CancelToken`. @@ -15950,6 +16598,36 @@ CancelToken.source = function source() { module.exports = CancelToken; +/***/ }), + +/***/ "../../node_modules/axios/lib/cancel/CanceledError.js": +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var AxiosError = __webpack_require__("../../node_modules/axios/lib/core/AxiosError.js"); +var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); + +/** + * A `CanceledError` is an object that is thrown when an operation is canceled. + * + * @class + * @param {string=} message The message. + */ +function CanceledError(message) { + // eslint-disable-next-line no-eq-null,eqeqeq + AxiosError.call(this, message == null ? 'canceled' : message, AxiosError.ERR_CANCELED); + this.name = 'CanceledError'; +} + +utils.inherits(CanceledError, AxiosError, { + __CANCEL__: true +}); + +module.exports = CanceledError; + + /***/ }), /***/ "../../node_modules/axios/lib/cancel/isCancel.js": @@ -15976,7 +16654,10 @@ var buildURL = __webpack_require__("../../node_modules/axios/lib/helpers/buildUR var InterceptorManager = __webpack_require__("../../node_modules/axios/lib/core/InterceptorManager.js"); var dispatchRequest = __webpack_require__("../../node_modules/axios/lib/core/dispatchRequest.js"); var mergeConfig = __webpack_require__("../../node_modules/axios/lib/core/mergeConfig.js"); +var buildFullPath = __webpack_require__("../../node_modules/axios/lib/core/buildFullPath.js"); +var validator = __webpack_require__("../../node_modules/axios/lib/helpers/validator.js"); +var validators = validator.validators; /** * Create a new instance of Axios * @@ -15995,14 +16676,14 @@ function Axios(instanceConfig) { * * @param {Object} config The config specific for this request (merged with this.defaults) */ -Axios.prototype.request = function request(config) { +Axios.prototype.request = function request(configOrUrl, config) { /*eslint no-param-reassign:0*/ // Allow for axios('example/url'[, config]) a la fetch API - if (typeof config === 'string') { - config = arguments[1] || {}; - config.url = arguments[0]; - } else { + if (typeof configOrUrl === 'string') { config = config || {}; + config.url = configOrUrl; + } else { + config = configOrUrl || {}; } config = mergeConfig(this.defaults, config); @@ -16016,20 +16697,71 @@ Axios.prototype.request = function request(config) { config.method = 'get'; } - // Hook up interceptors middleware - var chain = [dispatchRequest, undefined]; - var promise = Promise.resolve(config); + var transitional = config.transitional; + + if (transitional !== undefined) { + validator.assertOptions(transitional, { + silentJSONParsing: validators.transitional(validators.boolean), + forcedJSONParsing: validators.transitional(validators.boolean), + clarifyTimeoutError: validators.transitional(validators.boolean) + }, false); + } + // filter out skipped interceptors + var requestInterceptorChain = []; + var synchronousRequestInterceptors = true; this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { - chain.unshift(interceptor.fulfilled, interceptor.rejected); + if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { + return; + } + + synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; + + requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); }); + var responseInterceptorChain = []; this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { - chain.push(interceptor.fulfilled, interceptor.rejected); + responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); - while (chain.length) { - promise = promise.then(chain.shift(), chain.shift()); + var promise; + + if (!synchronousRequestInterceptors) { + var chain = [dispatchRequest, undefined]; + + Array.prototype.unshift.apply(chain, requestInterceptorChain); + chain = chain.concat(responseInterceptorChain); + + promise = Promise.resolve(config); + while (chain.length) { + promise = promise.then(chain.shift(), chain.shift()); + } + + return promise; + } + + + var newConfig = config; + while (requestInterceptorChain.length) { + var onFulfilled = requestInterceptorChain.shift(); + var onRejected = requestInterceptorChain.shift(); + try { + newConfig = onFulfilled(newConfig); + } catch (error) { + onRejected(error); + break; + } + } + + try { + promise = dispatchRequest(newConfig); + } catch (error) { + return Promise.reject(error); + } + + while (responseInterceptorChain.length) { + promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); } return promise; @@ -16037,7 +16769,8 @@ Axios.prototype.request = function request(config) { Axios.prototype.getUri = function getUri(config) { config = mergeConfig(this.defaults, config); - return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, ''); + var fullPath = buildFullPath(config.baseURL, config.url); + return buildURL(fullPath, config.params, config.paramsSerializer); }; // Provide aliases for supported request methods @@ -16054,18 +16787,122 @@ utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { /*eslint func-names:0*/ - Axios.prototype[method] = function(url, data, config) { - return this.request(mergeConfig(config || {}, { - method: method, - url: url, - data: data - })); - }; + + function generateHTTPMethod(isForm) { + return function httpMethod(url, data, config) { + return this.request(mergeConfig(config || {}, { + method: method, + headers: isForm ? { + 'Content-Type': 'multipart/form-data' + } : {}, + url: url, + data: data + })); + }; + } + + Axios.prototype[method] = generateHTTPMethod(); + + Axios.prototype[method + 'Form'] = generateHTTPMethod(true); }); module.exports = Axios; +/***/ }), + +/***/ "../../node_modules/axios/lib/core/AxiosError.js": +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); + +/** + * Create an Error with the specified message, config, error code, request and response. + * + * @param {string} message The error message. + * @param {string} [code] The error code (for example, 'ECONNABORTED'). + * @param {Object} [config] The config. + * @param {Object} [request] The request. + * @param {Object} [response] The response. + * @returns {Error} The created error. + */ +function AxiosError(message, code, config, request, response) { + Error.call(this); + this.message = message; + this.name = 'AxiosError'; + code && (this.code = code); + config && (this.config = config); + request && (this.request = request); + response && (this.response = response); +} + +utils.inherits(AxiosError, Error, { + toJSON: function toJSON() { + return { + // Standard + message: this.message, + name: this.name, + // Microsoft + description: this.description, + number: this.number, + // Mozilla + fileName: this.fileName, + lineNumber: this.lineNumber, + columnNumber: this.columnNumber, + stack: this.stack, + // Axios + config: this.config, + code: this.code, + status: this.response && this.response.status ? this.response.status : null + }; + } +}); + +var prototype = AxiosError.prototype; +var descriptors = {}; + +[ + 'ERR_BAD_OPTION_VALUE', + 'ERR_BAD_OPTION', + 'ECONNABORTED', + 'ETIMEDOUT', + 'ERR_NETWORK', + 'ERR_FR_TOO_MANY_REDIRECTS', + 'ERR_DEPRECATED', + 'ERR_BAD_RESPONSE', + 'ERR_BAD_REQUEST', + 'ERR_CANCELED' +// eslint-disable-next-line func-names +].forEach(function(code) { + descriptors[code] = {value: code}; +}); + +Object.defineProperties(AxiosError, descriptors); +Object.defineProperty(prototype, 'isAxiosError', {value: true}); + +// eslint-disable-next-line func-names +AxiosError.from = function(error, code, config, request, response, customProps) { + var axiosError = Object.create(prototype); + + utils.toFlatObject(error, axiosError, function filter(obj) { + return obj !== Error.prototype; + }); + + AxiosError.call(axiosError, error.message, code, config, request, response); + + axiosError.name = error.name; + + customProps && Object.assign(axiosError, customProps); + + return axiosError; +}; + +module.exports = AxiosError; + + /***/ }), /***/ "../../node_modules/axios/lib/core/InterceptorManager.js": @@ -16088,10 +16925,12 @@ function InterceptorManager() { * * @return {Number} An ID used to remove interceptor later */ -InterceptorManager.prototype.use = function use(fulfilled, rejected) { +InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { this.handlers.push({ fulfilled: fulfilled, - rejected: rejected + rejected: rejected, + synchronous: options ? options.synchronous : false, + runWhen: options ? options.runWhen : null }); return this.handlers.length - 1; }; @@ -16154,32 +16993,6 @@ module.exports = function buildFullPath(baseURL, requestedURL) { }; -/***/ }), - -/***/ "../../node_modules/axios/lib/core/createError.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -var enhanceError = __webpack_require__("../../node_modules/axios/lib/core/enhanceError.js"); - -/** - * Create an Error with the specified message, config, error code, request and response. - * - * @param {string} message The error message. - * @param {Object} config The config. - * @param {string} [code] The error code (for example, 'ECONNABORTED'). - * @param {Object} [request] The request. - * @param {Object} [response] The response. - * @returns {Error} The created error. - */ -module.exports = function createError(message, config, code, request, response) { - var error = new Error(message); - return enhanceError(error, config, code, request, response); -}; - - /***/ }), /***/ "../../node_modules/axios/lib/core/dispatchRequest.js": @@ -16191,15 +17004,20 @@ module.exports = function createError(message, config, code, request, response) var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); var transformData = __webpack_require__("../../node_modules/axios/lib/core/transformData.js"); var isCancel = __webpack_require__("../../node_modules/axios/lib/cancel/isCancel.js"); -var defaults = __webpack_require__("../../node_modules/axios/lib/defaults.js"); +var defaults = __webpack_require__("../../node_modules/axios/lib/defaults/index.js"); +var CanceledError = __webpack_require__("../../node_modules/axios/lib/cancel/CanceledError.js"); /** - * Throws a `Cancel` if cancellation has been requested. + * Throws a `CanceledError` if cancellation has been requested. */ function throwIfCancellationRequested(config) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); } + + if (config.signal && config.signal.aborted) { + throw new CanceledError(); + } } /** @@ -16215,7 +17033,8 @@ module.exports = function dispatchRequest(config) { config.headers = config.headers || {}; // Transform request data - config.data = transformData( + config.data = transformData.call( + config, config.data, config.headers, config.transformRequest @@ -16241,7 +17060,8 @@ module.exports = function dispatchRequest(config) { throwIfCancellationRequested(config); // Transform response data - response.data = transformData( + response.data = transformData.call( + config, response.data, response.headers, config.transformResponse @@ -16254,7 +17074,8 @@ module.exports = function dispatchRequest(config) { // Transform response data if (reason && reason.response) { - reason.response.data = transformData( + reason.response.data = transformData.call( + config, reason.response.data, reason.response.headers, config.transformResponse @@ -16267,56 +17088,6 @@ module.exports = function dispatchRequest(config) { }; -/***/ }), - -/***/ "../../node_modules/axios/lib/core/enhanceError.js": -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -/** - * Update an Error with the specified config, error code, and response. - * - * @param {Error} error The error to update. - * @param {Object} config The config. - * @param {string} [code] The error code (for example, 'ECONNABORTED'). - * @param {Object} [request] The request. - * @param {Object} [response] The response. - * @returns {Error} The error. - */ -module.exports = function enhanceError(error, config, code, request, response) { - error.config = config; - if (code) { - error.code = code; - } - - error.request = request; - error.response = response; - error.isAxiosError = true; - - error.toJSON = function toJSON() { - return { - // Standard - message: this.message, - name: this.name, - // Microsoft - description: this.description, - number: this.number, - // Mozilla - fileName: this.fileName, - lineNumber: this.lineNumber, - columnNumber: this.columnNumber, - stack: this.stack, - // Axios - config: this.config, - code: this.code - }; - }; - return error; -}; - - /***/ }), /***/ "../../node_modules/axios/lib/core/mergeConfig.js": @@ -16340,17 +17111,6 @@ module.exports = function mergeConfig(config1, config2) { config2 = config2 || {}; var config = {}; - var valueFromConfig2Keys = ['url', 'method', 'data']; - var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params']; - var defaultToConfig2Keys = [ - 'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer', - 'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', - 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress', - 'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent', - 'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding' - ]; - var directMergeKeys = ['validateStatus']; - function getMergedValue(target, source) { if (utils.isPlainObject(target) && utils.isPlainObject(source)) { return utils.merge(target, source); @@ -16362,51 +17122,75 @@ module.exports = function mergeConfig(config1, config2) { return source; } + // eslint-disable-next-line consistent-return function mergeDeepProperties(prop) { if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(config1[prop], config2[prop]); + return getMergedValue(config1[prop], config2[prop]); } else if (!utils.isUndefined(config1[prop])) { - config[prop] = getMergedValue(undefined, config1[prop]); + return getMergedValue(undefined, config1[prop]); } } - utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) { + // eslint-disable-next-line consistent-return + function valueFromConfig2(prop) { if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(undefined, config2[prop]); + return getMergedValue(undefined, config2[prop]); } - }); - - utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties); + } - utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) { + // eslint-disable-next-line consistent-return + function defaultToConfig2(prop) { if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(undefined, config2[prop]); + return getMergedValue(undefined, config2[prop]); } else if (!utils.isUndefined(config1[prop])) { - config[prop] = getMergedValue(undefined, config1[prop]); + return getMergedValue(undefined, config1[prop]); } - }); + } - utils.forEach(directMergeKeys, function merge(prop) { + // eslint-disable-next-line consistent-return + function mergeDirectKeys(prop) { if (prop in config2) { - config[prop] = getMergedValue(config1[prop], config2[prop]); + return getMergedValue(config1[prop], config2[prop]); } else if (prop in config1) { - config[prop] = getMergedValue(undefined, config1[prop]); - } - }); - - var axiosKeys = valueFromConfig2Keys - .concat(mergeDeepPropertiesKeys) - .concat(defaultToConfig2Keys) - .concat(directMergeKeys); - - var otherKeys = Object - .keys(config1) - .concat(Object.keys(config2)) - .filter(function filterAxiosKeys(key) { - return axiosKeys.indexOf(key) === -1; - }); + return getMergedValue(undefined, config1[prop]); + } + } + + var mergeMap = { + 'url': valueFromConfig2, + 'method': valueFromConfig2, + 'data': valueFromConfig2, + 'baseURL': defaultToConfig2, + 'transformRequest': defaultToConfig2, + 'transformResponse': defaultToConfig2, + 'paramsSerializer': defaultToConfig2, + 'timeout': defaultToConfig2, + 'timeoutMessage': defaultToConfig2, + 'withCredentials': defaultToConfig2, + 'adapter': defaultToConfig2, + 'responseType': defaultToConfig2, + 'xsrfCookieName': defaultToConfig2, + 'xsrfHeaderName': defaultToConfig2, + 'onUploadProgress': defaultToConfig2, + 'onDownloadProgress': defaultToConfig2, + 'decompress': defaultToConfig2, + 'maxContentLength': defaultToConfig2, + 'maxBodyLength': defaultToConfig2, + 'beforeRedirect': defaultToConfig2, + 'transport': defaultToConfig2, + 'httpAgent': defaultToConfig2, + 'httpsAgent': defaultToConfig2, + 'cancelToken': defaultToConfig2, + 'socketPath': defaultToConfig2, + 'responseEncoding': defaultToConfig2, + 'validateStatus': mergeDirectKeys + }; - utils.forEach(otherKeys, mergeDeepProperties); + utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) { + var merge = mergeMap[prop] || mergeDeepProperties; + var configValue = merge(prop); + (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue); + }); return config; }; @@ -16420,7 +17204,7 @@ module.exports = function mergeConfig(config1, config2) { "use strict"; -var createError = __webpack_require__("../../node_modules/axios/lib/core/createError.js"); +var AxiosError = __webpack_require__("../../node_modules/axios/lib/core/AxiosError.js"); /** * Resolve or reject a Promise based on response status. @@ -16434,10 +17218,10 @@ module.exports = function settle(resolve, reject, response) { if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { - reject(createError( + reject(new AxiosError( 'Request failed with status code ' + response.status, + [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4], response.config, - null, response.request, response )); @@ -16454,6 +17238,7 @@ module.exports = function settle(resolve, reject, response) { var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); +var defaults = __webpack_require__("../../node_modules/axios/lib/defaults/index.js"); /** * Transform the data for a request or a response @@ -16464,9 +17249,10 @@ var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); * @returns {*} The resulting transformed data */ module.exports = function transformData(data, headers, fns) { + var context = this || defaults; /*eslint no-param-reassign:0*/ utils.forEach(fns, function transform(fn) { - data = fn(data, headers); + data = fn.call(context, data, headers); }); return data; @@ -16475,7 +17261,16 @@ module.exports = function transformData(data, headers, fns) { /***/ }), -/***/ "../../node_modules/axios/lib/defaults.js": +/***/ "../../node_modules/axios/lib/defaults/env/FormData.js": +/***/ (function(module, exports, __webpack_require__) { + +// eslint-disable-next-line strict +module.exports = __webpack_require__("../../node_modules/form-data/lib/form_data.js"); + + +/***/ }), + +/***/ "../../node_modules/axios/lib/defaults/index.js": /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -16483,6 +17278,9 @@ module.exports = function transformData(data, headers, fns) { var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); var normalizeHeaderName = __webpack_require__("../../node_modules/axios/lib/helpers/normalizeHeaderName.js"); +var AxiosError = __webpack_require__("../../node_modules/axios/lib/core/AxiosError.js"); +var transitionalDefaults = __webpack_require__("../../node_modules/axios/lib/defaults/transitional.js"); +var toFormData = __webpack_require__("../../node_modules/axios/lib/helpers/toFormData.js"); var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' @@ -16506,12 +17304,31 @@ function getDefaultAdapter() { return adapter; } +function stringifySafely(rawValue, parser, encoder) { + if (utils.isString(rawValue)) { + try { + (parser || JSON.parse)(rawValue); + return utils.trim(rawValue); + } catch (e) { + if (e.name !== 'SyntaxError') { + throw e; + } + } + } + + return (encoder || JSON.stringify)(rawValue); +} + var defaults = { + + transitional: transitionalDefaults, + adapter: getDefaultAdapter(), transformRequest: [function transformRequest(data, headers) { normalizeHeaderName(headers, 'Accept'); normalizeHeaderName(headers, 'Content-Type'); + if (utils.isFormData(data) || utils.isArrayBuffer(data) || utils.isBuffer(data) || @@ -16528,20 +17345,42 @@ var defaults = { setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8'); return data.toString(); } - if (utils.isObject(data)) { - setContentTypeIfUnset(headers, 'application/json;charset=utf-8'); - return JSON.stringify(data); + + var isObjectPayload = utils.isObject(data); + var contentType = headers && headers['Content-Type']; + + var isFileList; + + if ((isFileList = utils.isFileList(data)) || (isObjectPayload && contentType === 'multipart/form-data')) { + var _FormData = this.env && this.env.FormData; + return toFormData(isFileList ? {'files[]': data} : data, _FormData && new _FormData()); + } else if (isObjectPayload || contentType === 'application/json') { + setContentTypeIfUnset(headers, 'application/json'); + return stringifySafely(data); } + return data; }], transformResponse: [function transformResponse(data) { - /*eslint no-param-reassign:0*/ - if (typeof data === 'string') { + var transitional = this.transitional || defaults.transitional; + var silentJSONParsing = transitional && transitional.silentJSONParsing; + var forcedJSONParsing = transitional && transitional.forcedJSONParsing; + var strictJSONParsing = !silentJSONParsing && this.responseType === 'json'; + + if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) { try { - data = JSON.parse(data); - } catch (e) { /* Ignore */ } + return JSON.parse(data); + } catch (e) { + if (strictJSONParsing) { + if (e.name === 'SyntaxError') { + throw AxiosError.from(e, AxiosError.ERR_BAD_RESPONSE, this, null, this.response); + } + throw e; + } + } } + return data; }], @@ -16557,14 +17396,18 @@ var defaults = { maxContentLength: -1, maxBodyLength: -1, + env: { + FormData: __webpack_require__("../../node_modules/axios/lib/defaults/env/FormData.js") + }, + validateStatus: function validateStatus(status) { return status >= 200 && status < 300; - } -}; + }, -defaults.headers = { - common: { - 'Accept': 'application/json, text/plain, */*' + headers: { + common: { + 'Accept': 'application/json, text/plain, */*' + } } }; @@ -16579,6 +17422,30 @@ utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { module.exports = defaults; +/***/ }), + +/***/ "../../node_modules/axios/lib/defaults/transitional.js": +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = { + silentJSONParsing: true, + forcedJSONParsing: true, + clarifyTimeoutError: false +}; + + +/***/ }), + +/***/ "../../node_modules/axios/lib/env/data.js": +/***/ (function(module, exports) { + +module.exports = { + "version": "0.27.2" +}; + /***/ }), /***/ "../../node_modules/axios/lib/helpers/bind.js": @@ -16777,7 +17644,7 @@ module.exports = function isAbsoluteURL(url) { // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. - return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); + return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url); }; @@ -16789,6 +17656,8 @@ module.exports = function isAbsoluteURL(url) { "use strict"; +var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); + /** * Determines whether the payload is an error thrown by Axios * @@ -16796,7 +17665,7 @@ module.exports = function isAbsoluteURL(url) { * @returns {boolean} True if the payload is an error thrown by Axios, otherwise false */ module.exports = function isAxiosError(payload) { - return (typeof payload === 'object') && (payload.isAxiosError === true); + return utils.isObject(payload) && (payload.isAxiosError === true); }; @@ -16957,6 +17826,20 @@ module.exports = function parseHeaders(headers) { }; +/***/ }), + +/***/ "../../node_modules/axios/lib/helpers/parseProtocol.js": +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = function parseProtocol(url) { + var match = /^([-+\w]{1,25})(:?\/\/|:)/.exec(url); + return match && match[1] || ''; +}; + + /***/ }), /***/ "../../node_modules/axios/lib/helpers/spread.js": @@ -16992,6 +17875,180 @@ module.exports = function spread(callback) { }; +/***/ }), + +/***/ "../../node_modules/axios/lib/helpers/toFormData.js": +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__("../../node_modules/axios/lib/utils.js"); + +/** + * Convert a data object to FormData + * @param {Object} obj + * @param {?Object} [formData] + * @returns {Object} + **/ + +function toFormData(obj, formData) { + // eslint-disable-next-line no-param-reassign + formData = formData || new FormData(); + + var stack = []; + + function convertValue(value) { + if (value === null) return ''; + + if (utils.isDate(value)) { + return value.toISOString(); + } + + if (utils.isArrayBuffer(value) || utils.isTypedArray(value)) { + return typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value); + } + + return value; + } + + function build(data, parentKey) { + if (utils.isPlainObject(data) || utils.isArray(data)) { + if (stack.indexOf(data) !== -1) { + throw Error('Circular reference detected in ' + parentKey); + } + + stack.push(data); + + utils.forEach(data, function each(value, key) { + if (utils.isUndefined(value)) return; + var fullKey = parentKey ? parentKey + '.' + key : key; + var arr; + + if (value && !parentKey && typeof value === 'object') { + if (utils.endsWith(key, '{}')) { + // eslint-disable-next-line no-param-reassign + value = JSON.stringify(value); + } else if (utils.endsWith(key, '[]') && (arr = utils.toArray(value))) { + // eslint-disable-next-line func-names + arr.forEach(function(el) { + !utils.isUndefined(el) && formData.append(fullKey, convertValue(el)); + }); + return; + } + } + + build(value, fullKey); + }); + + stack.pop(); + } else { + formData.append(parentKey, convertValue(data)); + } + } + + build(obj); + + return formData; +} + +module.exports = toFormData; + + +/***/ }), + +/***/ "../../node_modules/axios/lib/helpers/validator.js": +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var VERSION = __webpack_require__("../../node_modules/axios/lib/env/data.js").version; +var AxiosError = __webpack_require__("../../node_modules/axios/lib/core/AxiosError.js"); + +var validators = {}; + +// eslint-disable-next-line func-names +['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach(function(type, i) { + validators[type] = function validator(thing) { + return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type; + }; +}); + +var deprecatedWarnings = {}; + +/** + * Transitional option validator + * @param {function|boolean?} validator - set to false if the transitional option has been removed + * @param {string?} version - deprecated version / removed since version + * @param {string?} message - some message with additional info + * @returns {function} + */ +validators.transitional = function transitional(validator, version, message) { + function formatMessage(opt, desc) { + return '[Axios v' + VERSION + '] Transitional option \'' + opt + '\'' + desc + (message ? '. ' + message : ''); + } + + // eslint-disable-next-line func-names + return function(value, opt, opts) { + if (validator === false) { + throw new AxiosError( + formatMessage(opt, ' has been removed' + (version ? ' in ' + version : '')), + AxiosError.ERR_DEPRECATED + ); + } + + if (version && !deprecatedWarnings[opt]) { + deprecatedWarnings[opt] = true; + // eslint-disable-next-line no-console + console.warn( + formatMessage( + opt, + ' has been deprecated since v' + version + ' and will be removed in the near future' + ) + ); + } + + return validator ? validator(value, opt, opts) : true; + }; +}; + +/** + * Assert object's properties type + * @param {object} options + * @param {object} schema + * @param {boolean?} allowUnknown + */ + +function assertOptions(options, schema, allowUnknown) { + if (typeof options !== 'object') { + throw new AxiosError('options must be an object', AxiosError.ERR_BAD_OPTION_VALUE); + } + var keys = Object.keys(options); + var i = keys.length; + while (i-- > 0) { + var opt = keys[i]; + var validator = schema[opt]; + if (validator) { + var value = options[opt]; + var result = value === undefined || validator(value, opt, options); + if (result !== true) { + throw new AxiosError('option ' + opt + ' must be ' + result, AxiosError.ERR_BAD_OPTION_VALUE); + } + continue; + } + if (allowUnknown !== true) { + throw new AxiosError('Unknown option ' + opt, AxiosError.ERR_BAD_OPTION); + } + } +} + +module.exports = { + assertOptions: assertOptions, + validators: validators +}; + + /***/ }), /***/ "../../node_modules/axios/lib/utils.js": @@ -17002,12 +18059,26 @@ module.exports = function spread(callback) { var bind = __webpack_require__("../../node_modules/axios/lib/helpers/bind.js"); -/*global toString:true*/ - // utils is a library of generic helper functions non-specific to axios var toString = Object.prototype.toString; +// eslint-disable-next-line func-names +var kindOf = (function(cache) { + // eslint-disable-next-line func-names + return function(thing) { + var str = toString.call(thing); + return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase()); + }; +})(Object.create(null)); + +function kindOfTest(type) { + type = type.toLowerCase(); + return function isKindOf(thing) { + return kindOf(thing) === type; + }; +} + /** * Determine if a value is an Array * @@ -17015,7 +18086,7 @@ var toString = Object.prototype.toString; * @returns {boolean} True if value is an Array, otherwise false */ function isArray(val) { - return toString.call(val) === '[object Array]'; + return Array.isArray(val); } /** @@ -17042,22 +18113,12 @@ function isBuffer(val) { /** * Determine if a value is an ArrayBuffer * + * @function * @param {Object} val The value to test * @returns {boolean} True if value is an ArrayBuffer, otherwise false */ -function isArrayBuffer(val) { - return toString.call(val) === '[object ArrayBuffer]'; -} +var isArrayBuffer = kindOfTest('ArrayBuffer'); -/** - * Determine if a value is a FormData - * - * @param {Object} val The value to test - * @returns {boolean} True if value is an FormData, otherwise false - */ -function isFormData(val) { - return (typeof FormData !== 'undefined') && (val instanceof FormData); -} /** * Determine if a value is a view on an ArrayBuffer @@ -17070,7 +18131,7 @@ function isArrayBufferView(val) { if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) { result = ArrayBuffer.isView(val); } else { - result = (val) && (val.buffer) && (val.buffer instanceof ArrayBuffer); + result = (val) && (val.buffer) && (isArrayBuffer(val.buffer)); } return result; } @@ -17112,7 +18173,7 @@ function isObject(val) { * @return {boolean} True if value is a plain Object, otherwise false */ function isPlainObject(val) { - if (toString.call(val) !== '[object Object]') { + if (kindOf(val) !== 'object') { return false; } @@ -17123,32 +18184,38 @@ function isPlainObject(val) { /** * Determine if a value is a Date * + * @function * @param {Object} val The value to test * @returns {boolean} True if value is a Date, otherwise false */ -function isDate(val) { - return toString.call(val) === '[object Date]'; -} +var isDate = kindOfTest('Date'); /** * Determine if a value is a File * + * @function * @param {Object} val The value to test * @returns {boolean} True if value is a File, otherwise false */ -function isFile(val) { - return toString.call(val) === '[object File]'; -} +var isFile = kindOfTest('File'); /** * Determine if a value is a Blob * + * @function * @param {Object} val The value to test * @returns {boolean} True if value is a Blob, otherwise false */ -function isBlob(val) { - return toString.call(val) === '[object Blob]'; -} +var isBlob = kindOfTest('Blob'); + +/** + * Determine if a value is a FileList + * + * @function + * @param {Object} val The value to test + * @returns {boolean} True if value is a File, otherwise false + */ +var isFileList = kindOfTest('FileList'); /** * Determine if a value is a Function @@ -17171,14 +18238,27 @@ function isStream(val) { } /** - * Determine if a value is a URLSearchParams object + * Determine if a value is a FormData * + * @param {Object} thing The value to test + * @returns {boolean} True if value is an FormData, otherwise false + */ +function isFormData(thing) { + var pattern = '[object FormData]'; + return thing && ( + (typeof FormData === 'function' && thing instanceof FormData) || + toString.call(thing) === pattern || + (isFunction(thing.toString) && thing.toString() === pattern) + ); +} + +/** + * Determine if a value is a URLSearchParams object + * @function * @param {Object} val The value to test * @returns {boolean} True if value is a URLSearchParams object, otherwise false */ -function isURLSearchParams(val) { - return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams; -} +var isURLSearchParams = kindOfTest('URLSearchParams'); /** * Trim excess whitespace off the beginning and end of a string @@ -17187,7 +18267,7 @@ function isURLSearchParams(val) { * @returns {String} The String freed of excess whitespace */ function trim(str) { - return str.replace(/^\s*/, '').replace(/\s*$/, ''); + return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); } /** @@ -17325,6 +18405,94 @@ function stripBOM(content) { return content; } +/** + * Inherit the prototype methods from one constructor into another + * @param {function} constructor + * @param {function} superConstructor + * @param {object} [props] + * @param {object} [descriptors] + */ + +function inherits(constructor, superConstructor, props, descriptors) { + constructor.prototype = Object.create(superConstructor.prototype, descriptors); + constructor.prototype.constructor = constructor; + props && Object.assign(constructor.prototype, props); +} + +/** + * Resolve object with deep prototype chain to a flat object + * @param {Object} sourceObj source object + * @param {Object} [destObj] + * @param {Function} [filter] + * @returns {Object} + */ + +function toFlatObject(sourceObj, destObj, filter) { + var props; + var i; + var prop; + var merged = {}; + + destObj = destObj || {}; + + do { + props = Object.getOwnPropertyNames(sourceObj); + i = props.length; + while (i-- > 0) { + prop = props[i]; + if (!merged[prop]) { + destObj[prop] = sourceObj[prop]; + merged[prop] = true; + } + } + sourceObj = Object.getPrototypeOf(sourceObj); + } while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype); + + return destObj; +} + +/* + * determines whether a string ends with the characters of a specified string + * @param {String} str + * @param {String} searchString + * @param {Number} [position= 0] + * @returns {boolean} + */ +function endsWith(str, searchString, position) { + str = String(str); + if (position === undefined || position > str.length) { + position = str.length; + } + position -= searchString.length; + var lastIndex = str.indexOf(searchString, position); + return lastIndex !== -1 && lastIndex === position; +} + + +/** + * Returns new array from array like object + * @param {*} [thing] + * @returns {Array} + */ +function toArray(thing) { + if (!thing) return null; + var i = thing.length; + if (isUndefined(i)) return null; + var arr = new Array(i); + while (i-- > 0) { + arr[i] = thing[i]; + } + return arr; +} + +// eslint-disable-next-line func-names +var isTypedArray = (function(TypedArray) { + // eslint-disable-next-line func-names + return function(thing) { + return TypedArray && thing instanceof TypedArray; + }; +})(typeof Uint8Array !== 'undefined' && Object.getPrototypeOf(Uint8Array)); + module.exports = { isArray: isArray, isArrayBuffer: isArrayBuffer, @@ -17347,17 +18515,18 @@ module.exports = { merge: merge, extend: extend, trim: trim, - stripBOM: stripBOM + stripBOM: stripBOM, + inherits: inherits, + toFlatObject: toFlatObject, + kindOf: kindOf, + kindOfTest: kindOfTest, + endsWith: endsWith, + toArray: toArray, + isTypedArray: isTypedArray, + isFileList: isFileList }; -/***/ }), - -/***/ "../../node_modules/axios/package.json": -/***/ (function(module) { - -module.exports = JSON.parse("{\"name\":\"axios\",\"version\":\"0.21.1\",\"description\":\"Promise based HTTP client for the browser and node.js\",\"main\":\"index.js\",\"scripts\":{\"test\":\"grunt test && bundlesize\",\"start\":\"node ./sandbox/server.js\",\"build\":\"NODE_ENV=production grunt build\",\"preversion\":\"npm test\",\"version\":\"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json\",\"postversion\":\"git push && git push --tags\",\"examples\":\"node ./examples/server.js\",\"coveralls\":\"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js\",\"fix\":\"eslint --fix lib/**/*.js\"},\"repository\":{\"type\":\"git\",\"url\":\"https://github.com/axios/axios.git\"},\"keywords\":[\"xhr\",\"http\",\"ajax\",\"promise\",\"node\"],\"author\":\"Matt Zabriskie\",\"license\":\"MIT\",\"bugs\":{\"url\":\"https://github.com/axios/axios/issues\"},\"homepage\":\"https://github.com/axios/axios\",\"devDependencies\":{\"bundlesize\":\"^0.17.0\",\"coveralls\":\"^3.0.0\",\"es6-promise\":\"^4.2.4\",\"grunt\":\"^1.0.2\",\"grunt-banner\":\"^0.6.0\",\"grunt-cli\":\"^1.2.0\",\"grunt-contrib-clean\":\"^1.1.0\",\"grunt-contrib-watch\":\"^1.0.0\",\"grunt-eslint\":\"^20.1.0\",\"grunt-karma\":\"^2.0.0\",\"grunt-mocha-test\":\"^0.13.3\",\"grunt-ts\":\"^6.0.0-beta.19\",\"grunt-webpack\":\"^1.0.18\",\"istanbul-instrumenter-loader\":\"^1.0.0\",\"jasmine-core\":\"^2.4.1\",\"karma\":\"^1.3.0\",\"karma-chrome-launcher\":\"^2.2.0\",\"karma-coverage\":\"^1.1.1\",\"karma-firefox-launcher\":\"^1.1.0\",\"karma-jasmine\":\"^1.1.1\",\"karma-jasmine-ajax\":\"^0.1.13\",\"karma-opera-launcher\":\"^1.0.0\",\"karma-safari-launcher\":\"^1.0.0\",\"karma-sauce-launcher\":\"^1.2.0\",\"karma-sinon\":\"^1.0.5\",\"karma-sourcemap-loader\":\"^0.3.7\",\"karma-webpack\":\"^1.7.0\",\"load-grunt-tasks\":\"^3.5.2\",\"minimist\":\"^1.2.0\",\"mocha\":\"^5.2.0\",\"sinon\":\"^4.5.0\",\"typescript\":\"^2.8.1\",\"url-search-params\":\"^0.10.0\",\"webpack\":\"^1.13.1\",\"webpack-dev-server\":\"^1.14.1\"},\"browser\":{\"./lib/adapters/http.js\":\"./lib/adapters/xhr.js\"},\"jsdelivr\":\"dist/axios.min.js\",\"unpkg\":\"dist/axios.min.js\",\"typings\":\"./index.d.ts\",\"dependencies\":{\"follow-redirects\":\"^1.10.0\"},\"bundlesize\":[{\"path\":\"./dist/axios.min.js\",\"threshold\":\"5kB\"}]}"); - /***/ }), /***/ "../../node_modules/balanced-match/index.js": @@ -20982,6 +22151,221 @@ module.exports = { }; +/***/ }), + +/***/ "../../node_modules/combined-stream/lib/combined_stream.js": +/***/ (function(module, exports, __webpack_require__) { + +var util = __webpack_require__("util"); +var Stream = __webpack_require__("stream").Stream; +var DelayedStream = __webpack_require__("../../node_modules/delayed-stream/lib/delayed_stream.js"); + +module.exports = CombinedStream; +function CombinedStream() { + this.writable = false; + this.readable = true; + this.dataSize = 0; + this.maxDataSize = 2 * 1024 * 1024; + this.pauseStreams = true; + + this._released = false; + this._streams = []; + this._currentStream = null; + this._insideLoop = false; + this._pendingNext = false; +} +util.inherits(CombinedStream, Stream); + +CombinedStream.create = function(options) { + var combinedStream = new this(); + + options = options || {}; + for (var option in options) { + combinedStream[option] = options[option]; + } + + return combinedStream; +}; + +CombinedStream.isStreamLike = function(stream) { + return (typeof stream !== 'function') + && (typeof stream !== 'string') + && (typeof stream !== 'boolean') + && (typeof stream !== 'number') + && (!Buffer.isBuffer(stream)); +}; + +CombinedStream.prototype.append = function(stream) { + var isStreamLike = CombinedStream.isStreamLike(stream); + + if (isStreamLike) { + if (!(stream instanceof DelayedStream)) { + var newStream = DelayedStream.create(stream, { + maxDataSize: Infinity, + pauseStream: this.pauseStreams, + }); + stream.on('data', this._checkDataSize.bind(this)); + stream = newStream; + } + + this._handleErrors(stream); + + if (this.pauseStreams) { + stream.pause(); + } + } + + this._streams.push(stream); + return this; +}; + +CombinedStream.prototype.pipe = function(dest, options) { + Stream.prototype.pipe.call(this, dest, options); + this.resume(); + return dest; +}; + +CombinedStream.prototype._getNext = function() { + this._currentStream = null; + + if (this._insideLoop) { + this._pendingNext = true; + return; // defer call + } + + this._insideLoop = true; + try { + do { + this._pendingNext = false; + this._realGetNext(); + } while (this._pendingNext); + } finally { + this._insideLoop = false; + } +}; + +CombinedStream.prototype._realGetNext = function() { + var stream = this._streams.shift(); + + + if (typeof stream == 'undefined') { + this.end(); + return; + } + + if (typeof stream !== 'function') { + this._pipeNext(stream); + return; + } + + var getStream = stream; + getStream(function(stream) { + var isStreamLike = CombinedStream.isStreamLike(stream); + if (isStreamLike) { + stream.on('data', this._checkDataSize.bind(this)); + this._handleErrors(stream); + } + + this._pipeNext(stream); + }.bind(this)); +}; + +CombinedStream.prototype._pipeNext = function(stream) { + this._currentStream = stream; + + var isStreamLike = CombinedStream.isStreamLike(stream); + if (isStreamLike) { + stream.on('end', this._getNext.bind(this)); + stream.pipe(this, {end: false}); + return; + } + + var value = stream; + this.write(value); + this._getNext(); +}; + +CombinedStream.prototype._handleErrors = function(stream) { + var self = this; + stream.on('error', function(err) { + self._emitError(err); + }); +}; + +CombinedStream.prototype.write = function(data) { + this.emit('data', data); +}; + +CombinedStream.prototype.pause = function() { + if (!this.pauseStreams) { + return; + } + + if(this.pauseStreams && this._currentStream && typeof(this._currentStream.pause) == 'function') this._currentStream.pause(); + this.emit('pause'); +}; + +CombinedStream.prototype.resume = function() { + if (!this._released) { + this._released = true; + this.writable = true; + this._getNext(); + } + + if(this.pauseStreams && this._currentStream && typeof(this._currentStream.resume) == 'function') this._currentStream.resume(); + this.emit('resume'); +}; + +CombinedStream.prototype.end = function() { + this._reset(); + this.emit('end'); +}; + +CombinedStream.prototype.destroy = function() { + this._reset(); + this.emit('close'); +}; + +CombinedStream.prototype._reset = function() { + this.writable = false; + this._streams = []; + this._currentStream = null; +}; + +CombinedStream.prototype._checkDataSize = function() { + this._updateDataSize(); + if (this.dataSize <= this.maxDataSize) { + return; + } + + var message = + 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.'; + this._emitError(new Error(message)); +}; + +CombinedStream.prototype._updateDataSize = function() { + this.dataSize = 0; + + var self = this; + this._streams.forEach(function(stream) { + if (!stream.dataSize) { + return; + } + + self.dataSize += stream.dataSize; + }); + + if (this._currentStream && this._currentStream.dataSize) { + this.dataSize += this._currentStream.dataSize; + } +}; + +CombinedStream.prototype._emitError = function(err) { + this._reset(); + this.emit('error', err); +}; + + /***/ }), /***/ "../../node_modules/concat-map/index.js": @@ -24644,6 +26028,120 @@ module.exports = async ( }; +/***/ }), + +/***/ "../../node_modules/delayed-stream/lib/delayed_stream.js": +/***/ (function(module, exports, __webpack_require__) { + +var Stream = __webpack_require__("stream").Stream; +var util = __webpack_require__("util"); + +module.exports = DelayedStream; +function DelayedStream() { + this.source = null; + this.dataSize = 0; + this.maxDataSize = 1024 * 1024; + this.pauseStream = true; + + this._maxDataSizeExceeded = false; + this._released = false; + this._bufferedEvents = []; +} +util.inherits(DelayedStream, Stream); + +DelayedStream.create = function(source, options) { + var delayedStream = new this(); + + options = options || {}; + for (var option in options) { + delayedStream[option] = options[option]; + } + + delayedStream.source = source; + + var realEmit = source.emit; + source.emit = function() { + delayedStream._handleEmit(arguments); + return realEmit.apply(source, arguments); + }; + + source.on('error', function() {}); + if (delayedStream.pauseStream) { + source.pause(); + } + + return delayedStream; +}; + +Object.defineProperty(DelayedStream.prototype, 'readable', { + configurable: true, + enumerable: true, + get: function() { + return this.source.readable; + } +}); + +DelayedStream.prototype.setEncoding = function() { + return this.source.setEncoding.apply(this.source, arguments); +}; + +DelayedStream.prototype.resume = function() { + if (!this._released) { + this.release(); + } + + this.source.resume(); +}; + +DelayedStream.prototype.pause = function() { + this.source.pause(); +}; + +DelayedStream.prototype.release = function() { + this._released = true; + + this._bufferedEvents.forEach(function(args) { + this.emit.apply(this, args); + }.bind(this)); + this._bufferedEvents = []; +}; + +DelayedStream.prototype.pipe = function() { + var r = Stream.prototype.pipe.apply(this, arguments); + this.resume(); + return r; +}; + +DelayedStream.prototype._handleEmit = function(args) { + if (this._released) { + this.emit.apply(this, args); + return; + } + + if (args[0] === 'data') { + this.dataSize += args[1].length; + this._checkIfMaxDataSizeExceeded(); + } + + this._bufferedEvents.push(args); +}; + +DelayedStream.prototype._checkIfMaxDataSizeExceeded = function() { + if (this._maxDataSizeExceeded) { + return; + } + + if (this.dataSize <= this.maxDataSize) { + return; + } + + this._maxDataSizeExceeded = true; + var message = + 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.' + this.emit('error', new Error(message)); +}; + + /***/ }), /***/ "../../node_modules/detect-indent/index.js": @@ -26759,7 +28257,7 @@ RedirectableRequest.prototype._performRequest = function () { // If specified, use the agent corresponding to the protocol // (HTTP and HTTPS use different types of agents) if (this._options.agents) { - var scheme = protocol.substr(0, protocol.length - 1); + var scheme = protocol.slice(0, -1); this._options.agent = this._options.agents[scheme]; } @@ -26851,10 +28349,21 @@ RedirectableRequest.prototype._processResponse = function (response) { return; } + // Store the request headers if applicable + var requestHeaders; + var beforeRedirect = this._options.beforeRedirect; + if (beforeRedirect) { + requestHeaders = Object.assign({ + // The Host header was set by nativeProtocol.request + Host: response.req.getHeader("host"), + }, this._options.headers); + } + // RFC7231§6.4: Automatic redirection needs to done with // care for methods not known to be safe, […] // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change // the request method from POST to GET for the subsequent request. + var method = this._options.method; if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" || // RFC7231§6.4.4: The 303 (See Other) status code indicates that // the server is redirecting the user agent to a different resource […] @@ -26902,10 +28411,18 @@ RedirectableRequest.prototype._processResponse = function (response) { } // Evaluate the beforeRedirect callback - if (typeof this._options.beforeRedirect === "function") { - var responseDetails = { headers: response.headers }; + if (typeof beforeRedirect === "function") { + var responseDetails = { + headers: response.headers, + statusCode: statusCode, + }; + var requestDetails = { + url: currentUrl, + method: method, + headers: requestHeaders, + }; try { - this._options.beforeRedirect.call(null, this._options, responseDetails); + beforeRedirect(this._options, responseDetails, requestDetails); } catch (err) { this.emit("error", err); @@ -27063,6 +28580,531 @@ module.exports = wrap({ http: http, https: https }); module.exports.wrap = wrap; +/***/ }), + +/***/ "../../node_modules/form-data/lib/form_data.js": +/***/ (function(module, exports, __webpack_require__) { + +var CombinedStream = __webpack_require__("../../node_modules/combined-stream/lib/combined_stream.js"); +var util = __webpack_require__("util"); +var path = __webpack_require__("path"); +var http = __webpack_require__("http"); +var https = __webpack_require__("https"); +var parseUrl = __webpack_require__("url").parse; +var fs = __webpack_require__("fs"); +var Stream = __webpack_require__("stream").Stream; +var mime = __webpack_require__("../../node_modules/mime-types/index.js"); +var asynckit = __webpack_require__("../../node_modules/asynckit/index.js"); +var populate = __webpack_require__("../../node_modules/form-data/lib/populate.js"); + +// Public API +module.exports = FormData; + +// make it a Stream +util.inherits(FormData, CombinedStream); + +/** + * Create readable "multipart/form-data" streams. + * Can be used to submit forms + * and file uploads to other web applications. + * + * @constructor + * @param {Object} options - Properties to be added/overriden for FormData and CombinedStream + */ +function FormData(options) { + if (!(this instanceof FormData)) { + return new FormData(options); + } + + this._overheadLength = 0; + this._valueLength = 0; + this._valuesToMeasure = []; + + CombinedStream.call(this); + + options = options || {}; + for (var option in options) { + this[option] = options[option]; + } +} + +FormData.LINE_BREAK = '\r\n'; +FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream'; + +FormData.prototype.append = function(field, value, options) { + + options = options || {}; + + // allow filename as single option + if (typeof options == 'string') { + options = {filename: options}; + } + + var append = CombinedStream.prototype.append.bind(this); + + // all that streamy business can't handle numbers + if (typeof value == 'number') { + value = '' + value; + } + + // https://github.com/felixge/node-form-data/issues/38 + if (util.isArray(value)) { + // Please convert your array into string + // the way web server expects it + this._error(new Error('Arrays are not supported.')); + return; + } + + var header = this._multiPartHeader(field, value, options); + var footer = this._multiPartFooter(); + + append(header); + append(value); + append(footer); + + // pass along options.knownLength + this._trackLength(header, value, options); +}; + +FormData.prototype._trackLength = function(header, value, options) { + var valueLength = 0; + + // used w/ getLengthSync(), when length is known. + // e.g. for streaming directly from a remote server, + // w/ a known file a size, and not wanting to wait for + // incoming file to finish to get its size. + if (options.knownLength != null) { + valueLength += +options.knownLength; + } else if (Buffer.isBuffer(value)) { + valueLength = value.length; + } else if (typeof value === 'string') { + valueLength = Buffer.byteLength(value); + } + + this._valueLength += valueLength; + + // @check why add CRLF? does this account for custom/multiple CRLFs? + this._overheadLength += + Buffer.byteLength(header) + + FormData.LINE_BREAK.length; + + // empty or either doesn't have path or not an http response or not a stream + if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) && !(value instanceof Stream))) { + return; + } + + // no need to bother with the length + if (!options.knownLength) { + this._valuesToMeasure.push(value); + } +}; + +FormData.prototype._lengthRetriever = function(value, callback) { + + if (value.hasOwnProperty('fd')) { + + // take read range into a account + // `end` = Infinity –> read file till the end + // + // TODO: Looks like there is bug in Node fs.createReadStream + // it doesn't respect `end` options without `start` options + // Fix it when node fixes it. + // https://github.com/joyent/node/issues/7819 + if (value.end != undefined && value.end != Infinity && value.start != undefined) { + + // when end specified + // no need to calculate range + // inclusive, starts with 0 + callback(null, value.end + 1 - (value.start ? value.start : 0)); + + // not that fast snoopy + } else { + // still need to fetch file size from fs + fs.stat(value.path, function(err, stat) { + + var fileSize; + + if (err) { + callback(err); + return; + } + + // update final size based on the range options + fileSize = stat.size - (value.start ? value.start : 0); + callback(null, fileSize); + }); + } + + // or http response + } else if (value.hasOwnProperty('httpVersion')) { + callback(null, +value.headers['content-length']); + + // or request stream http://github.com/mikeal/request + } else if (value.hasOwnProperty('httpModule')) { + // wait till response come back + value.on('response', function(response) { + value.pause(); + callback(null, +response.headers['content-length']); + }); + value.resume(); + + // something else + } else { + callback('Unknown stream'); + } +}; + +FormData.prototype._multiPartHeader = function(field, value, options) { + // custom header specified (as string)? + // it becomes responsible for boundary + // (e.g. to handle extra CRLFs on .NET servers) + if (typeof options.header == 'string') { + return options.header; + } + + var contentDisposition = this._getContentDisposition(value, options); + var contentType = this._getContentType(value, options); + + var contents = ''; + var headers = { + // add custom disposition as third element or keep it two elements if not + 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []), + // if no content type. allow it to be empty array + 'Content-Type': [].concat(contentType || []) + }; + + // allow custom headers. + if (typeof options.header == 'object') { + populate(headers, options.header); + } + + var header; + for (var prop in headers) { + if (!headers.hasOwnProperty(prop)) continue; + header = headers[prop]; + + // skip nullish headers. + if (header == null) { + continue; + } + + // convert all headers to arrays. + if (!Array.isArray(header)) { + header = [header]; + } + + // add non-empty headers. + if (header.length) { + contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK; + } + } + + return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK; +}; + +FormData.prototype._getContentDisposition = function(value, options) { + + var filename + , contentDisposition + ; + + if (typeof options.filepath === 'string') { + // custom filepath for relative paths + filename = path.normalize(options.filepath).replace(/\\/g, '/'); + } else if (options.filename || value.name || value.path) { + // custom filename take precedence + // formidable and the browser add a name property + // fs- and request- streams have path property + filename = path.basename(options.filename || value.name || value.path); + } else if (value.readable && value.hasOwnProperty('httpVersion')) { + // or try http response + filename = path.basename(value.client._httpMessage.path || ''); + } + + if (filename) { + contentDisposition = 'filename="' + filename + '"'; + } + + return contentDisposition; +}; + +FormData.prototype._getContentType = function(value, options) { + + // use custom content-type above all + var contentType = options.contentType; + + // or try `name` from formidable, browser + if (!contentType && value.name) { + contentType = mime.lookup(value.name); + } + + // or try `path` from fs-, request- streams + if (!contentType && value.path) { + contentType = mime.lookup(value.path); + } + + // or if it's http-reponse + if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) { + contentType = value.headers['content-type']; + } + + // or guess it from the filepath or filename + if (!contentType && (options.filepath || options.filename)) { + contentType = mime.lookup(options.filepath || options.filename); + } + + // fallback to the default content type if `value` is not simple value + if (!contentType && typeof value == 'object') { + contentType = FormData.DEFAULT_CONTENT_TYPE; + } + + return contentType; +}; + +FormData.prototype._multiPartFooter = function() { + return function(next) { + var footer = FormData.LINE_BREAK; + + var lastPart = (this._streams.length === 0); + if (lastPart) { + footer += this._lastBoundary(); + } + + next(footer); + }.bind(this); +}; + +FormData.prototype._lastBoundary = function() { + return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK; +}; + +FormData.prototype.getHeaders = function(userHeaders) { + var header; + var formHeaders = { + 'content-type': 'multipart/form-data; boundary=' + this.getBoundary() + }; + + for (header in userHeaders) { + if (userHeaders.hasOwnProperty(header)) { + formHeaders[header.toLowerCase()] = userHeaders[header]; + } + } + + return formHeaders; +}; + +FormData.prototype.setBoundary = function(boundary) { + this._boundary = boundary; +}; + +FormData.prototype.getBoundary = function() { + if (!this._boundary) { + this._generateBoundary(); + } + + return this._boundary; +}; + +FormData.prototype.getBuffer = function() { + var dataBuffer = new Buffer.alloc( 0 ); + var boundary = this.getBoundary(); + + // Create the form content. Add Line breaks to the end of data. + for (var i = 0, len = this._streams.length; i < len; i++) { + if (typeof this._streams[i] !== 'function') { + + // Add content to the buffer. + if(Buffer.isBuffer(this._streams[i])) { + dataBuffer = Buffer.concat( [dataBuffer, this._streams[i]]); + }else { + dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(this._streams[i])]); + } + + // Add break after content. + if (typeof this._streams[i] !== 'string' || this._streams[i].substring( 2, boundary.length + 2 ) !== boundary) { + dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(FormData.LINE_BREAK)] ); + } + } + } + + // Add the footer and return the Buffer object. + return Buffer.concat( [dataBuffer, Buffer.from(this._lastBoundary())] ); +}; + +FormData.prototype._generateBoundary = function() { + // This generates a 50 character boundary similar to those used by Firefox. + // They are optimized for boyer-moore parsing. + var boundary = '--------------------------'; + for (var i = 0; i < 24; i++) { + boundary += Math.floor(Math.random() * 10).toString(16); + } + + this._boundary = boundary; +}; + +// Note: getLengthSync DOESN'T calculate streams length +// As workaround one can calculate file size manually +// and add it as knownLength option +FormData.prototype.getLengthSync = function() { + var knownLength = this._overheadLength + this._valueLength; + + // Don't get confused, there are 3 "internal" streams for each keyval pair + // so it basically checks if there is any value added to the form + if (this._streams.length) { + knownLength += this._lastBoundary().length; + } + + // https://github.com/form-data/form-data/issues/40 + if (!this.hasKnownLength()) { + // Some async length retrievers are present + // therefore synchronous length calculation is false. + // Please use getLength(callback) to get proper length + this._error(new Error('Cannot calculate proper length in synchronous way.')); + } + + return knownLength; +}; + +// Public API to check if length of added values is known +// https://github.com/form-data/form-data/issues/196 +// https://github.com/form-data/form-data/issues/262 +FormData.prototype.hasKnownLength = function() { + var hasKnownLength = true; + + if (this._valuesToMeasure.length) { + hasKnownLength = false; + } + + return hasKnownLength; +}; + +FormData.prototype.getLength = function(cb) { + var knownLength = this._overheadLength + this._valueLength; + + if (this._streams.length) { + knownLength += this._lastBoundary().length; + } + + if (!this._valuesToMeasure.length) { + process.nextTick(cb.bind(this, null, knownLength)); + return; + } + + asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) { + if (err) { + cb(err); + return; + } + + values.forEach(function(length) { + knownLength += length; + }); + + cb(null, knownLength); + }); +}; + +FormData.prototype.submit = function(params, cb) { + var request + , options + , defaults = {method: 'post'} + ; + + // parse provided url if it's string + // or treat it as options object + if (typeof params == 'string') { + + params = parseUrl(params); + options = populate({ + port: params.port, + path: params.pathname, + host: params.hostname, + protocol: params.protocol + }, defaults); + + // use custom params + } else { + + options = populate(params, defaults); + // if no port provided use default one + if (!options.port) { + options.port = options.protocol == 'https:' ? 443 : 80; + } + } + + // put that good code in getHeaders to some use + options.headers = this.getHeaders(params.headers); + + // https if specified, fallback to http in any other case + if (options.protocol == 'https:') { + request = https.request(options); + } else { + request = http.request(options); + } + + // get content length and fire away + this.getLength(function(err, length) { + if (err && err !== 'Unknown stream') { + this._error(err); + return; + } + + // add content length + if (length) { + request.setHeader('Content-Length', length); + } + + this.pipe(request); + if (cb) { + var onResponse; + + var callback = function (error, responce) { + request.removeListener('error', callback); + request.removeListener('response', onResponse); + + return cb.call(this, error, responce); + }; + + onResponse = callback.bind(this, null); + + request.on('error', callback); + request.on('response', onResponse); + } + }.bind(this)); + + return request; +}; + +FormData.prototype._error = function(err) { + if (!this.error) { + this.error = err; + this.pause(); + this.emit('error', err); + } +}; + +FormData.prototype.toString = function () { + return '[object FormData]'; +}; + + +/***/ }), + +/***/ "../../node_modules/form-data/lib/populate.js": +/***/ (function(module, exports) { + +// populates missing values +module.exports = function(dst, src) { + + Object.keys(src).forEach(function(prop) + { + dst[prop] = dst[prop] || src[prop]; + }); + + return dst; +}; + + /***/ }), /***/ "../../node_modules/fs.realpath/index.js": @@ -34967,6 +37009,227 @@ function pauseStreams (streams, options) { } +/***/ }), + +/***/ "../../node_modules/mime-db/db.json": +/***/ (function(module) { + +module.exports = JSON.parse("{\"application/1d-interleaved-parityfec\":{\"source\":\"iana\"},\"application/3gpdash-qoe-report+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/3gpp-ims+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/a2l\":{\"source\":\"iana\"},\"application/activemessage\":{\"source\":\"iana\"},\"application/activity+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-costmap+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-costmapfilter+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-directory+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-endpointcost+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-endpointcostparams+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-endpointprop+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-endpointpropparams+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-error+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-networkmap+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-networkmapfilter+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-updatestreamcontrol+json\":{\"source\":\"iana\",\"compressible\":true},\"application/alto-updatestreamparams+json\":{\"source\":\"iana\",\"compressible\":true},\"application/aml\":{\"source\":\"iana\"},\"application/andrew-inset\":{\"source\":\"iana\",\"extensions\":[\"ez\"]},\"application/applefile\":{\"source\":\"iana\"},\"application/applixware\":{\"source\":\"apache\",\"extensions\":[\"aw\"]},\"application/atf\":{\"source\":\"iana\"},\"application/atfx\":{\"source\":\"iana\"},\"application/atom+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"atom\"]},\"application/atomcat+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"atomcat\"]},\"application/atomdeleted+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"atomdeleted\"]},\"application/atomicmail\":{\"source\":\"iana\"},\"application/atomsvc+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"atomsvc\"]},\"application/atsc-dwd+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"dwd\"]},\"application/atsc-dynamic-event-message\":{\"source\":\"iana\"},\"application/atsc-held+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"held\"]},\"application/atsc-rdt+json\":{\"source\":\"iana\",\"compressible\":true},\"application/atsc-rsat+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rsat\"]},\"application/atxml\":{\"source\":\"iana\"},\"application/auth-policy+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/bacnet-xdd+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/batch-smtp\":{\"source\":\"iana\"},\"application/bdoc\":{\"compressible\":false,\"extensions\":[\"bdoc\"]},\"application/beep+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/calendar+json\":{\"source\":\"iana\",\"compressible\":true},\"application/calendar+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xcs\"]},\"application/call-completion\":{\"source\":\"iana\"},\"application/cals-1840\":{\"source\":\"iana\"},\"application/cap+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/cbor\":{\"source\":\"iana\"},\"application/cbor-seq\":{\"source\":\"iana\"},\"application/cccex\":{\"source\":\"iana\"},\"application/ccmp+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/ccxml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"ccxml\"]},\"application/cdfx+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"cdfx\"]},\"application/cdmi-capability\":{\"source\":\"iana\",\"extensions\":[\"cdmia\"]},\"application/cdmi-container\":{\"source\":\"iana\",\"extensions\":[\"cdmic\"]},\"application/cdmi-domain\":{\"source\":\"iana\",\"extensions\":[\"cdmid\"]},\"application/cdmi-object\":{\"source\":\"iana\",\"extensions\":[\"cdmio\"]},\"application/cdmi-queue\":{\"source\":\"iana\",\"extensions\":[\"cdmiq\"]},\"application/cdni\":{\"source\":\"iana\"},\"application/cea\":{\"source\":\"iana\"},\"application/cea-2018+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/cellml+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/cfw\":{\"source\":\"iana\"},\"application/clue+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/clue_info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/cms\":{\"source\":\"iana\"},\"application/cnrp+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/coap-group+json\":{\"source\":\"iana\",\"compressible\":true},\"application/coap-payload\":{\"source\":\"iana\"},\"application/commonground\":{\"source\":\"iana\"},\"application/conference-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/cose\":{\"source\":\"iana\"},\"application/cose-key\":{\"source\":\"iana\"},\"application/cose-key-set\":{\"source\":\"iana\"},\"application/cpl+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/csrattrs\":{\"source\":\"iana\"},\"application/csta+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/cstadata+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/csvm+json\":{\"source\":\"iana\",\"compressible\":true},\"application/cu-seeme\":{\"source\":\"apache\",\"extensions\":[\"cu\"]},\"application/cwt\":{\"source\":\"iana\"},\"application/cybercash\":{\"source\":\"iana\"},\"application/dart\":{\"compressible\":true},\"application/dash+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mpd\"]},\"application/dashdelta\":{\"source\":\"iana\"},\"application/davmount+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"davmount\"]},\"application/dca-rft\":{\"source\":\"iana\"},\"application/dcd\":{\"source\":\"iana\"},\"application/dec-dx\":{\"source\":\"iana\"},\"application/dialog-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/dicom\":{\"source\":\"iana\"},\"application/dicom+json\":{\"source\":\"iana\",\"compressible\":true},\"application/dicom+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/dii\":{\"source\":\"iana\"},\"application/dit\":{\"source\":\"iana\"},\"application/dns\":{\"source\":\"iana\"},\"application/dns+json\":{\"source\":\"iana\",\"compressible\":true},\"application/dns-message\":{\"source\":\"iana\"},\"application/docbook+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"dbk\"]},\"application/dots+cbor\":{\"source\":\"iana\"},\"application/dskpp+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/dssc+der\":{\"source\":\"iana\",\"extensions\":[\"dssc\"]},\"application/dssc+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xdssc\"]},\"application/dvcs\":{\"source\":\"iana\"},\"application/ecmascript\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"ecma\",\"es\"]},\"application/edi-consent\":{\"source\":\"iana\"},\"application/edi-x12\":{\"source\":\"iana\",\"compressible\":false},\"application/edifact\":{\"source\":\"iana\",\"compressible\":false},\"application/efi\":{\"source\":\"iana\"},\"application/emergencycalldata.comment+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/emergencycalldata.control+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/emergencycalldata.deviceinfo+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/emergencycalldata.ecall.msd\":{\"source\":\"iana\"},\"application/emergencycalldata.providerinfo+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/emergencycalldata.serviceinfo+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/emergencycalldata.subscriberinfo+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/emergencycalldata.veds+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/emma+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"emma\"]},\"application/emotionml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"emotionml\"]},\"application/encaprtp\":{\"source\":\"iana\"},\"application/epp+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/epub+zip\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"epub\"]},\"application/eshop\":{\"source\":\"iana\"},\"application/exi\":{\"source\":\"iana\",\"extensions\":[\"exi\"]},\"application/expect-ct-report+json\":{\"source\":\"iana\",\"compressible\":true},\"application/fastinfoset\":{\"source\":\"iana\"},\"application/fastsoap\":{\"source\":\"iana\"},\"application/fdt+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"fdt\"]},\"application/fhir+json\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/fhir+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/fido.trusted-apps+json\":{\"compressible\":true},\"application/fits\":{\"source\":\"iana\"},\"application/flexfec\":{\"source\":\"iana\"},\"application/font-sfnt\":{\"source\":\"iana\"},\"application/font-tdpfr\":{\"source\":\"iana\",\"extensions\":[\"pfr\"]},\"application/font-woff\":{\"source\":\"iana\",\"compressible\":false},\"application/framework-attributes+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/geo+json\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"geojson\"]},\"application/geo+json-seq\":{\"source\":\"iana\"},\"application/geopackage+sqlite3\":{\"source\":\"iana\"},\"application/geoxacml+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/gltf-buffer\":{\"source\":\"iana\"},\"application/gml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"gml\"]},\"application/gpx+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"gpx\"]},\"application/gxf\":{\"source\":\"apache\",\"extensions\":[\"gxf\"]},\"application/gzip\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"gz\"]},\"application/h224\":{\"source\":\"iana\"},\"application/held+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/hjson\":{\"extensions\":[\"hjson\"]},\"application/http\":{\"source\":\"iana\"},\"application/hyperstudio\":{\"source\":\"iana\",\"extensions\":[\"stk\"]},\"application/ibe-key-request+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/ibe-pkg-reply+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/ibe-pp-data\":{\"source\":\"iana\"},\"application/iges\":{\"source\":\"iana\"},\"application/im-iscomposing+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/index\":{\"source\":\"iana\"},\"application/index.cmd\":{\"source\":\"iana\"},\"application/index.obj\":{\"source\":\"iana\"},\"application/index.response\":{\"source\":\"iana\"},\"application/index.vnd\":{\"source\":\"iana\"},\"application/inkml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"ink\",\"inkml\"]},\"application/iotp\":{\"source\":\"iana\"},\"application/ipfix\":{\"source\":\"iana\",\"extensions\":[\"ipfix\"]},\"application/ipp\":{\"source\":\"iana\"},\"application/isup\":{\"source\":\"iana\"},\"application/its+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"its\"]},\"application/java-archive\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"jar\",\"war\",\"ear\"]},\"application/java-serialized-object\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"ser\"]},\"application/java-vm\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"class\"]},\"application/javascript\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"js\",\"mjs\"]},\"application/jf2feed+json\":{\"source\":\"iana\",\"compressible\":true},\"application/jose\":{\"source\":\"iana\"},\"application/jose+json\":{\"source\":\"iana\",\"compressible\":true},\"application/jrd+json\":{\"source\":\"iana\",\"compressible\":true},\"application/json\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"json\",\"map\"]},\"application/json-patch+json\":{\"source\":\"iana\",\"compressible\":true},\"application/json-seq\":{\"source\":\"iana\"},\"application/json5\":{\"extensions\":[\"json5\"]},\"application/jsonml+json\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"jsonml\"]},\"application/jwk+json\":{\"source\":\"iana\",\"compressible\":true},\"application/jwk-set+json\":{\"source\":\"iana\",\"compressible\":true},\"application/jwt\":{\"source\":\"iana\"},\"application/kpml-request+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/kpml-response+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/ld+json\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"jsonld\"]},\"application/lgr+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"lgr\"]},\"application/link-format\":{\"source\":\"iana\"},\"application/load-control+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/lost+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"lostxml\"]},\"application/lostsync+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/lpf+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/lxf\":{\"source\":\"iana\"},\"application/mac-binhex40\":{\"source\":\"iana\",\"extensions\":[\"hqx\"]},\"application/mac-compactpro\":{\"source\":\"apache\",\"extensions\":[\"cpt\"]},\"application/macwriteii\":{\"source\":\"iana\"},\"application/mads+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mads\"]},\"application/manifest+json\":{\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"webmanifest\"]},\"application/marc\":{\"source\":\"iana\",\"extensions\":[\"mrc\"]},\"application/marcxml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mrcx\"]},\"application/mathematica\":{\"source\":\"iana\",\"extensions\":[\"ma\",\"nb\",\"mb\"]},\"application/mathml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mathml\"]},\"application/mathml-content+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mathml-presentation+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-associated-procedure-description+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-deregister+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-envelope+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-msk+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-msk-response+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-protection-description+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-reception-report+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-register+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-register-response+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-schedule+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbms-user-service-description+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mbox\":{\"source\":\"iana\",\"extensions\":[\"mbox\"]},\"application/media-policy-dataset+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/media_control+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/mediaservercontrol+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mscml\"]},\"application/merge-patch+json\":{\"source\":\"iana\",\"compressible\":true},\"application/metalink+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"metalink\"]},\"application/metalink4+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"meta4\"]},\"application/mets+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mets\"]},\"application/mf4\":{\"source\":\"iana\"},\"application/mikey\":{\"source\":\"iana\"},\"application/mipc\":{\"source\":\"iana\"},\"application/mmt-aei+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"maei\"]},\"application/mmt-usd+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"musd\"]},\"application/mods+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mods\"]},\"application/moss-keys\":{\"source\":\"iana\"},\"application/moss-signature\":{\"source\":\"iana\"},\"application/mosskey-data\":{\"source\":\"iana\"},\"application/mosskey-request\":{\"source\":\"iana\"},\"application/mp21\":{\"source\":\"iana\",\"extensions\":[\"m21\",\"mp21\"]},\"application/mp4\":{\"source\":\"iana\",\"extensions\":[\"mp4s\",\"m4p\"]},\"application/mpeg4-generic\":{\"source\":\"iana\"},\"application/mpeg4-iod\":{\"source\":\"iana\"},\"application/mpeg4-iod-xmt\":{\"source\":\"iana\"},\"application/mrb-consumer+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xdf\"]},\"application/mrb-publish+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xdf\"]},\"application/msc-ivr+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/msc-mixer+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/msword\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"doc\",\"dot\"]},\"application/mud+json\":{\"source\":\"iana\",\"compressible\":true},\"application/multipart-core\":{\"source\":\"iana\"},\"application/mxf\":{\"source\":\"iana\",\"extensions\":[\"mxf\"]},\"application/n-quads\":{\"source\":\"iana\",\"extensions\":[\"nq\"]},\"application/n-triples\":{\"source\":\"iana\",\"extensions\":[\"nt\"]},\"application/nasdata\":{\"source\":\"iana\"},\"application/news-checkgroups\":{\"source\":\"iana\",\"charset\":\"US-ASCII\"},\"application/news-groupinfo\":{\"source\":\"iana\",\"charset\":\"US-ASCII\"},\"application/news-transmission\":{\"source\":\"iana\"},\"application/nlsml+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/node\":{\"source\":\"iana\",\"extensions\":[\"cjs\"]},\"application/nss\":{\"source\":\"iana\"},\"application/ocsp-request\":{\"source\":\"iana\"},\"application/ocsp-response\":{\"source\":\"iana\"},\"application/octet-stream\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"bin\",\"dms\",\"lrf\",\"mar\",\"so\",\"dist\",\"distz\",\"pkg\",\"bpk\",\"dump\",\"elc\",\"deploy\",\"exe\",\"dll\",\"deb\",\"dmg\",\"iso\",\"img\",\"msi\",\"msp\",\"msm\",\"buffer\"]},\"application/oda\":{\"source\":\"iana\",\"extensions\":[\"oda\"]},\"application/odm+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/odx\":{\"source\":\"iana\"},\"application/oebps-package+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"opf\"]},\"application/ogg\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"ogx\"]},\"application/omdoc+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"omdoc\"]},\"application/onenote\":{\"source\":\"apache\",\"extensions\":[\"onetoc\",\"onetoc2\",\"onetmp\",\"onepkg\"]},\"application/oscore\":{\"source\":\"iana\"},\"application/oxps\":{\"source\":\"iana\",\"extensions\":[\"oxps\"]},\"application/p2p-overlay+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"relo\"]},\"application/parityfec\":{\"source\":\"iana\"},\"application/passport\":{\"source\":\"iana\"},\"application/patch-ops-error+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xer\"]},\"application/pdf\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"pdf\"]},\"application/pdx\":{\"source\":\"iana\"},\"application/pem-certificate-chain\":{\"source\":\"iana\"},\"application/pgp-encrypted\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"pgp\"]},\"application/pgp-keys\":{\"source\":\"iana\"},\"application/pgp-signature\":{\"source\":\"iana\",\"extensions\":[\"asc\",\"sig\"]},\"application/pics-rules\":{\"source\":\"apache\",\"extensions\":[\"prf\"]},\"application/pidf+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/pidf-diff+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/pkcs10\":{\"source\":\"iana\",\"extensions\":[\"p10\"]},\"application/pkcs12\":{\"source\":\"iana\"},\"application/pkcs7-mime\":{\"source\":\"iana\",\"extensions\":[\"p7m\",\"p7c\"]},\"application/pkcs7-signature\":{\"source\":\"iana\",\"extensions\":[\"p7s\"]},\"application/pkcs8\":{\"source\":\"iana\",\"extensions\":[\"p8\"]},\"application/pkcs8-encrypted\":{\"source\":\"iana\"},\"application/pkix-attr-cert\":{\"source\":\"iana\",\"extensions\":[\"ac\"]},\"application/pkix-cert\":{\"source\":\"iana\",\"extensions\":[\"cer\"]},\"application/pkix-crl\":{\"source\":\"iana\",\"extensions\":[\"crl\"]},\"application/pkix-pkipath\":{\"source\":\"iana\",\"extensions\":[\"pkipath\"]},\"application/pkixcmp\":{\"source\":\"iana\",\"extensions\":[\"pki\"]},\"application/pls+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"pls\"]},\"application/poc-settings+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/postscript\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"ai\",\"eps\",\"ps\"]},\"application/ppsp-tracker+json\":{\"source\":\"iana\",\"compressible\":true},\"application/problem+json\":{\"source\":\"iana\",\"compressible\":true},\"application/problem+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/provenance+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"provx\"]},\"application/prs.alvestrand.titrax-sheet\":{\"source\":\"iana\"},\"application/prs.cww\":{\"source\":\"iana\",\"extensions\":[\"cww\"]},\"application/prs.hpub+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/prs.nprend\":{\"source\":\"iana\"},\"application/prs.plucker\":{\"source\":\"iana\"},\"application/prs.rdf-xml-crypt\":{\"source\":\"iana\"},\"application/prs.xsf+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/pskc+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"pskcxml\"]},\"application/pvd+json\":{\"source\":\"iana\",\"compressible\":true},\"application/qsig\":{\"source\":\"iana\"},\"application/raml+yaml\":{\"compressible\":true,\"extensions\":[\"raml\"]},\"application/raptorfec\":{\"source\":\"iana\"},\"application/rdap+json\":{\"source\":\"iana\",\"compressible\":true},\"application/rdf+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rdf\",\"owl\"]},\"application/reginfo+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rif\"]},\"application/relax-ng-compact-syntax\":{\"source\":\"iana\",\"extensions\":[\"rnc\"]},\"application/remote-printing\":{\"source\":\"iana\"},\"application/reputon+json\":{\"source\":\"iana\",\"compressible\":true},\"application/resource-lists+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rl\"]},\"application/resource-lists-diff+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rld\"]},\"application/rfc+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/riscos\":{\"source\":\"iana\"},\"application/rlmi+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/rls-services+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rs\"]},\"application/route-apd+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rapd\"]},\"application/route-s-tsid+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"sls\"]},\"application/route-usd+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rusd\"]},\"application/rpki-ghostbusters\":{\"source\":\"iana\",\"extensions\":[\"gbr\"]},\"application/rpki-manifest\":{\"source\":\"iana\",\"extensions\":[\"mft\"]},\"application/rpki-publication\":{\"source\":\"iana\"},\"application/rpki-roa\":{\"source\":\"iana\",\"extensions\":[\"roa\"]},\"application/rpki-updown\":{\"source\":\"iana\"},\"application/rsd+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"rsd\"]},\"application/rss+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"rss\"]},\"application/rtf\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rtf\"]},\"application/rtploopback\":{\"source\":\"iana\"},\"application/rtx\":{\"source\":\"iana\"},\"application/samlassertion+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/samlmetadata+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/sbe\":{\"source\":\"iana\"},\"application/sbml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"sbml\"]},\"application/scaip+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/scim+json\":{\"source\":\"iana\",\"compressible\":true},\"application/scvp-cv-request\":{\"source\":\"iana\",\"extensions\":[\"scq\"]},\"application/scvp-cv-response\":{\"source\":\"iana\",\"extensions\":[\"scs\"]},\"application/scvp-vp-request\":{\"source\":\"iana\",\"extensions\":[\"spq\"]},\"application/scvp-vp-response\":{\"source\":\"iana\",\"extensions\":[\"spp\"]},\"application/sdp\":{\"source\":\"iana\",\"extensions\":[\"sdp\"]},\"application/secevent+jwt\":{\"source\":\"iana\"},\"application/senml+cbor\":{\"source\":\"iana\"},\"application/senml+json\":{\"source\":\"iana\",\"compressible\":true},\"application/senml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"senmlx\"]},\"application/senml-etch+cbor\":{\"source\":\"iana\"},\"application/senml-etch+json\":{\"source\":\"iana\",\"compressible\":true},\"application/senml-exi\":{\"source\":\"iana\"},\"application/sensml+cbor\":{\"source\":\"iana\"},\"application/sensml+json\":{\"source\":\"iana\",\"compressible\":true},\"application/sensml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"sensmlx\"]},\"application/sensml-exi\":{\"source\":\"iana\"},\"application/sep+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/sep-exi\":{\"source\":\"iana\"},\"application/session-info\":{\"source\":\"iana\"},\"application/set-payment\":{\"source\":\"iana\"},\"application/set-payment-initiation\":{\"source\":\"iana\",\"extensions\":[\"setpay\"]},\"application/set-registration\":{\"source\":\"iana\"},\"application/set-registration-initiation\":{\"source\":\"iana\",\"extensions\":[\"setreg\"]},\"application/sgml\":{\"source\":\"iana\"},\"application/sgml-open-catalog\":{\"source\":\"iana\"},\"application/shf+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"shf\"]},\"application/sieve\":{\"source\":\"iana\",\"extensions\":[\"siv\",\"sieve\"]},\"application/simple-filter+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/simple-message-summary\":{\"source\":\"iana\"},\"application/simplesymbolcontainer\":{\"source\":\"iana\"},\"application/sipc\":{\"source\":\"iana\"},\"application/slate\":{\"source\":\"iana\"},\"application/smil\":{\"source\":\"iana\"},\"application/smil+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"smi\",\"smil\"]},\"application/smpte336m\":{\"source\":\"iana\"},\"application/soap+fastinfoset\":{\"source\":\"iana\"},\"application/soap+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/sparql-query\":{\"source\":\"iana\",\"extensions\":[\"rq\"]},\"application/sparql-results+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"srx\"]},\"application/spirits-event+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/sql\":{\"source\":\"iana\"},\"application/srgs\":{\"source\":\"iana\",\"extensions\":[\"gram\"]},\"application/srgs+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"grxml\"]},\"application/sru+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"sru\"]},\"application/ssdl+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"ssdl\"]},\"application/ssml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"ssml\"]},\"application/stix+json\":{\"source\":\"iana\",\"compressible\":true},\"application/swid+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"swidtag\"]},\"application/tamp-apex-update\":{\"source\":\"iana\"},\"application/tamp-apex-update-confirm\":{\"source\":\"iana\"},\"application/tamp-community-update\":{\"source\":\"iana\"},\"application/tamp-community-update-confirm\":{\"source\":\"iana\"},\"application/tamp-error\":{\"source\":\"iana\"},\"application/tamp-sequence-adjust\":{\"source\":\"iana\"},\"application/tamp-sequence-adjust-confirm\":{\"source\":\"iana\"},\"application/tamp-status-query\":{\"source\":\"iana\"},\"application/tamp-status-response\":{\"source\":\"iana\"},\"application/tamp-update\":{\"source\":\"iana\"},\"application/tamp-update-confirm\":{\"source\":\"iana\"},\"application/tar\":{\"compressible\":true},\"application/taxii+json\":{\"source\":\"iana\",\"compressible\":true},\"application/td+json\":{\"source\":\"iana\",\"compressible\":true},\"application/tei+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"tei\",\"teicorpus\"]},\"application/tetra_isi\":{\"source\":\"iana\"},\"application/thraud+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"tfi\"]},\"application/timestamp-query\":{\"source\":\"iana\"},\"application/timestamp-reply\":{\"source\":\"iana\"},\"application/timestamped-data\":{\"source\":\"iana\",\"extensions\":[\"tsd\"]},\"application/tlsrpt+gzip\":{\"source\":\"iana\"},\"application/tlsrpt+json\":{\"source\":\"iana\",\"compressible\":true},\"application/tnauthlist\":{\"source\":\"iana\"},\"application/toml\":{\"compressible\":true,\"extensions\":[\"toml\"]},\"application/trickle-ice-sdpfrag\":{\"source\":\"iana\"},\"application/trig\":{\"source\":\"iana\"},\"application/ttml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"ttml\"]},\"application/tve-trigger\":{\"source\":\"iana\"},\"application/tzif\":{\"source\":\"iana\"},\"application/tzif-leap\":{\"source\":\"iana\"},\"application/ulpfec\":{\"source\":\"iana\"},\"application/urc-grpsheet+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/urc-ressheet+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rsheet\"]},\"application/urc-targetdesc+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/urc-uisocketdesc+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vcard+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vcard+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vemmi\":{\"source\":\"iana\"},\"application/vividence.scriptfile\":{\"source\":\"apache\"},\"application/vnd.1000minds.decision-model+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"1km\"]},\"application/vnd.3gpp-prose+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp-prose-pc3ch+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp-v2x-local-service-information\":{\"source\":\"iana\"},\"application/vnd.3gpp.access-transfer-events+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.bsf+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.gmop+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mc-signalling-ear\":{\"source\":\"iana\"},\"application/vnd.3gpp.mcdata-affiliation-command+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcdata-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcdata-payload\":{\"source\":\"iana\"},\"application/vnd.3gpp.mcdata-service-config+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcdata-signalling\":{\"source\":\"iana\"},\"application/vnd.3gpp.mcdata-ue-config+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcdata-user-profile+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-affiliation-command+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-floor-request+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-location-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-mbms-usage-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-service-config+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-signed+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-ue-config+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-ue-init-config+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcptt-user-profile+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-affiliation-command+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-affiliation-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-location-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-mbms-usage-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-service-config+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-transmission-request+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-ue-config+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mcvideo-user-profile+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.mid-call+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.pic-bw-large\":{\"source\":\"iana\",\"extensions\":[\"plb\"]},\"application/vnd.3gpp.pic-bw-small\":{\"source\":\"iana\",\"extensions\":[\"psb\"]},\"application/vnd.3gpp.pic-bw-var\":{\"source\":\"iana\",\"extensions\":[\"pvb\"]},\"application/vnd.3gpp.sms\":{\"source\":\"iana\"},\"application/vnd.3gpp.sms+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.srvcc-ext+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.srvcc-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.state-and-event-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp.ussd+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp2.bcmcsinfo+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.3gpp2.sms\":{\"source\":\"iana\"},\"application/vnd.3gpp2.tcap\":{\"source\":\"iana\",\"extensions\":[\"tcap\"]},\"application/vnd.3lightssoftware.imagescal\":{\"source\":\"iana\"},\"application/vnd.3m.post-it-notes\":{\"source\":\"iana\",\"extensions\":[\"pwn\"]},\"application/vnd.accpac.simply.aso\":{\"source\":\"iana\",\"extensions\":[\"aso\"]},\"application/vnd.accpac.simply.imp\":{\"source\":\"iana\",\"extensions\":[\"imp\"]},\"application/vnd.acucobol\":{\"source\":\"iana\",\"extensions\":[\"acu\"]},\"application/vnd.acucorp\":{\"source\":\"iana\",\"extensions\":[\"atc\",\"acutc\"]},\"application/vnd.adobe.air-application-installer-package+zip\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"air\"]},\"application/vnd.adobe.flash.movie\":{\"source\":\"iana\"},\"application/vnd.adobe.formscentral.fcdt\":{\"source\":\"iana\",\"extensions\":[\"fcdt\"]},\"application/vnd.adobe.fxp\":{\"source\":\"iana\",\"extensions\":[\"fxp\",\"fxpl\"]},\"application/vnd.adobe.partial-upload\":{\"source\":\"iana\"},\"application/vnd.adobe.xdp+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xdp\"]},\"application/vnd.adobe.xfdf\":{\"source\":\"iana\",\"extensions\":[\"xfdf\"]},\"application/vnd.aether.imp\":{\"source\":\"iana\"},\"application/vnd.afpc.afplinedata\":{\"source\":\"iana\"},\"application/vnd.afpc.afplinedata-pagedef\":{\"source\":\"iana\"},\"application/vnd.afpc.foca-charset\":{\"source\":\"iana\"},\"application/vnd.afpc.foca-codedfont\":{\"source\":\"iana\"},\"application/vnd.afpc.foca-codepage\":{\"source\":\"iana\"},\"application/vnd.afpc.modca\":{\"source\":\"iana\"},\"application/vnd.afpc.modca-formdef\":{\"source\":\"iana\"},\"application/vnd.afpc.modca-mediummap\":{\"source\":\"iana\"},\"application/vnd.afpc.modca-objectcontainer\":{\"source\":\"iana\"},\"application/vnd.afpc.modca-overlay\":{\"source\":\"iana\"},\"application/vnd.afpc.modca-pagesegment\":{\"source\":\"iana\"},\"application/vnd.ah-barcode\":{\"source\":\"iana\"},\"application/vnd.ahead.space\":{\"source\":\"iana\",\"extensions\":[\"ahead\"]},\"application/vnd.airzip.filesecure.azf\":{\"source\":\"iana\",\"extensions\":[\"azf\"]},\"application/vnd.airzip.filesecure.azs\":{\"source\":\"iana\",\"extensions\":[\"azs\"]},\"application/vnd.amadeus+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.amazon.ebook\":{\"source\":\"apache\",\"extensions\":[\"azw\"]},\"application/vnd.amazon.mobi8-ebook\":{\"source\":\"iana\"},\"application/vnd.americandynamics.acc\":{\"source\":\"iana\",\"extensions\":[\"acc\"]},\"application/vnd.amiga.ami\":{\"source\":\"iana\",\"extensions\":[\"ami\"]},\"application/vnd.amundsen.maze+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.android.ota\":{\"source\":\"iana\"},\"application/vnd.android.package-archive\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"apk\"]},\"application/vnd.anki\":{\"source\":\"iana\"},\"application/vnd.anser-web-certificate-issue-initiation\":{\"source\":\"iana\",\"extensions\":[\"cii\"]},\"application/vnd.anser-web-funds-transfer-initiation\":{\"source\":\"apache\",\"extensions\":[\"fti\"]},\"application/vnd.antix.game-component\":{\"source\":\"iana\",\"extensions\":[\"atx\"]},\"application/vnd.apache.thrift.binary\":{\"source\":\"iana\"},\"application/vnd.apache.thrift.compact\":{\"source\":\"iana\"},\"application/vnd.apache.thrift.json\":{\"source\":\"iana\"},\"application/vnd.api+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.aplextor.warrp+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.apothekende.reservation+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.apple.installer+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mpkg\"]},\"application/vnd.apple.keynote\":{\"source\":\"iana\",\"extensions\":[\"keynote\"]},\"application/vnd.apple.mpegurl\":{\"source\":\"iana\",\"extensions\":[\"m3u8\"]},\"application/vnd.apple.numbers\":{\"source\":\"iana\",\"extensions\":[\"numbers\"]},\"application/vnd.apple.pages\":{\"source\":\"iana\",\"extensions\":[\"pages\"]},\"application/vnd.apple.pkpass\":{\"compressible\":false,\"extensions\":[\"pkpass\"]},\"application/vnd.arastra.swi\":{\"source\":\"iana\"},\"application/vnd.aristanetworks.swi\":{\"source\":\"iana\",\"extensions\":[\"swi\"]},\"application/vnd.artisan+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.artsquare\":{\"source\":\"iana\"},\"application/vnd.astraea-software.iota\":{\"source\":\"iana\",\"extensions\":[\"iota\"]},\"application/vnd.audiograph\":{\"source\":\"iana\",\"extensions\":[\"aep\"]},\"application/vnd.autopackage\":{\"source\":\"iana\"},\"application/vnd.avalon+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.avistar+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.balsamiq.bmml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"bmml\"]},\"application/vnd.balsamiq.bmpr\":{\"source\":\"iana\"},\"application/vnd.banana-accounting\":{\"source\":\"iana\"},\"application/vnd.bbf.usp.error\":{\"source\":\"iana\"},\"application/vnd.bbf.usp.msg\":{\"source\":\"iana\"},\"application/vnd.bbf.usp.msg+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.bekitzur-stech+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.bint.med-content\":{\"source\":\"iana\"},\"application/vnd.biopax.rdf+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.blink-idb-value-wrapper\":{\"source\":\"iana\"},\"application/vnd.blueice.multipass\":{\"source\":\"iana\",\"extensions\":[\"mpm\"]},\"application/vnd.bluetooth.ep.oob\":{\"source\":\"iana\"},\"application/vnd.bluetooth.le.oob\":{\"source\":\"iana\"},\"application/vnd.bmi\":{\"source\":\"iana\",\"extensions\":[\"bmi\"]},\"application/vnd.bpf\":{\"source\":\"iana\"},\"application/vnd.bpf3\":{\"source\":\"iana\"},\"application/vnd.businessobjects\":{\"source\":\"iana\",\"extensions\":[\"rep\"]},\"application/vnd.byu.uapi+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.cab-jscript\":{\"source\":\"iana\"},\"application/vnd.canon-cpdl\":{\"source\":\"iana\"},\"application/vnd.canon-lips\":{\"source\":\"iana\"},\"application/vnd.capasystems-pg+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.cendio.thinlinc.clientconf\":{\"source\":\"iana\"},\"application/vnd.century-systems.tcp_stream\":{\"source\":\"iana\"},\"application/vnd.chemdraw+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"cdxml\"]},\"application/vnd.chess-pgn\":{\"source\":\"iana\"},\"application/vnd.chipnuts.karaoke-mmd\":{\"source\":\"iana\",\"extensions\":[\"mmd\"]},\"application/vnd.ciedi\":{\"source\":\"iana\"},\"application/vnd.cinderella\":{\"source\":\"iana\",\"extensions\":[\"cdy\"]},\"application/vnd.cirpack.isdn-ext\":{\"source\":\"iana\"},\"application/vnd.citationstyles.style+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"csl\"]},\"application/vnd.claymore\":{\"source\":\"iana\",\"extensions\":[\"cla\"]},\"application/vnd.cloanto.rp9\":{\"source\":\"iana\",\"extensions\":[\"rp9\"]},\"application/vnd.clonk.c4group\":{\"source\":\"iana\",\"extensions\":[\"c4g\",\"c4d\",\"c4f\",\"c4p\",\"c4u\"]},\"application/vnd.cluetrust.cartomobile-config\":{\"source\":\"iana\",\"extensions\":[\"c11amc\"]},\"application/vnd.cluetrust.cartomobile-config-pkg\":{\"source\":\"iana\",\"extensions\":[\"c11amz\"]},\"application/vnd.coffeescript\":{\"source\":\"iana\"},\"application/vnd.collabio.xodocuments.document\":{\"source\":\"iana\"},\"application/vnd.collabio.xodocuments.document-template\":{\"source\":\"iana\"},\"application/vnd.collabio.xodocuments.presentation\":{\"source\":\"iana\"},\"application/vnd.collabio.xodocuments.presentation-template\":{\"source\":\"iana\"},\"application/vnd.collabio.xodocuments.spreadsheet\":{\"source\":\"iana\"},\"application/vnd.collabio.xodocuments.spreadsheet-template\":{\"source\":\"iana\"},\"application/vnd.collection+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.collection.doc+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.collection.next+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.comicbook+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.comicbook-rar\":{\"source\":\"iana\"},\"application/vnd.commerce-battelle\":{\"source\":\"iana\"},\"application/vnd.commonspace\":{\"source\":\"iana\",\"extensions\":[\"csp\"]},\"application/vnd.contact.cmsg\":{\"source\":\"iana\",\"extensions\":[\"cdbcmsg\"]},\"application/vnd.coreos.ignition+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.cosmocaller\":{\"source\":\"iana\",\"extensions\":[\"cmc\"]},\"application/vnd.crick.clicker\":{\"source\":\"iana\",\"extensions\":[\"clkx\"]},\"application/vnd.crick.clicker.keyboard\":{\"source\":\"iana\",\"extensions\":[\"clkk\"]},\"application/vnd.crick.clicker.palette\":{\"source\":\"iana\",\"extensions\":[\"clkp\"]},\"application/vnd.crick.clicker.template\":{\"source\":\"iana\",\"extensions\":[\"clkt\"]},\"application/vnd.crick.clicker.wordbank\":{\"source\":\"iana\",\"extensions\":[\"clkw\"]},\"application/vnd.criticaltools.wbs+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"wbs\"]},\"application/vnd.cryptii.pipe+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.crypto-shade-file\":{\"source\":\"iana\"},\"application/vnd.ctc-posml\":{\"source\":\"iana\",\"extensions\":[\"pml\"]},\"application/vnd.ctct.ws+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.cups-pdf\":{\"source\":\"iana\"},\"application/vnd.cups-postscript\":{\"source\":\"iana\"},\"application/vnd.cups-ppd\":{\"source\":\"iana\",\"extensions\":[\"ppd\"]},\"application/vnd.cups-raster\":{\"source\":\"iana\"},\"application/vnd.cups-raw\":{\"source\":\"iana\"},\"application/vnd.curl\":{\"source\":\"iana\"},\"application/vnd.curl.car\":{\"source\":\"apache\",\"extensions\":[\"car\"]},\"application/vnd.curl.pcurl\":{\"source\":\"apache\",\"extensions\":[\"pcurl\"]},\"application/vnd.cyan.dean.root+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.cybank\":{\"source\":\"iana\"},\"application/vnd.d2l.coursepackage1p0+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.dart\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"dart\"]},\"application/vnd.data-vision.rdz\":{\"source\":\"iana\",\"extensions\":[\"rdz\"]},\"application/vnd.datapackage+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dataresource+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dbf\":{\"source\":\"iana\"},\"application/vnd.debian.binary-package\":{\"source\":\"iana\"},\"application/vnd.dece.data\":{\"source\":\"iana\",\"extensions\":[\"uvf\",\"uvvf\",\"uvd\",\"uvvd\"]},\"application/vnd.dece.ttml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"uvt\",\"uvvt\"]},\"application/vnd.dece.unspecified\":{\"source\":\"iana\",\"extensions\":[\"uvx\",\"uvvx\"]},\"application/vnd.dece.zip\":{\"source\":\"iana\",\"extensions\":[\"uvz\",\"uvvz\"]},\"application/vnd.denovo.fcselayout-link\":{\"source\":\"iana\",\"extensions\":[\"fe_launch\"]},\"application/vnd.desmume.movie\":{\"source\":\"iana\"},\"application/vnd.dir-bi.plate-dl-nosuffix\":{\"source\":\"iana\"},\"application/vnd.dm.delegation+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dna\":{\"source\":\"iana\",\"extensions\":[\"dna\"]},\"application/vnd.document+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dolby.mlp\":{\"source\":\"apache\",\"extensions\":[\"mlp\"]},\"application/vnd.dolby.mobile.1\":{\"source\":\"iana\"},\"application/vnd.dolby.mobile.2\":{\"source\":\"iana\"},\"application/vnd.doremir.scorecloud-binary-document\":{\"source\":\"iana\"},\"application/vnd.dpgraph\":{\"source\":\"iana\",\"extensions\":[\"dpg\"]},\"application/vnd.dreamfactory\":{\"source\":\"iana\",\"extensions\":[\"dfac\"]},\"application/vnd.drive+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ds-keypoint\":{\"source\":\"apache\",\"extensions\":[\"kpxx\"]},\"application/vnd.dtg.local\":{\"source\":\"iana\"},\"application/vnd.dtg.local.flash\":{\"source\":\"iana\"},\"application/vnd.dtg.local.html\":{\"source\":\"iana\"},\"application/vnd.dvb.ait\":{\"source\":\"iana\",\"extensions\":[\"ait\"]},\"application/vnd.dvb.dvbisl+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dvb.dvbj\":{\"source\":\"iana\"},\"application/vnd.dvb.esgcontainer\":{\"source\":\"iana\"},\"application/vnd.dvb.ipdcdftnotifaccess\":{\"source\":\"iana\"},\"application/vnd.dvb.ipdcesgaccess\":{\"source\":\"iana\"},\"application/vnd.dvb.ipdcesgaccess2\":{\"source\":\"iana\"},\"application/vnd.dvb.ipdcesgpdd\":{\"source\":\"iana\"},\"application/vnd.dvb.ipdcroaming\":{\"source\":\"iana\"},\"application/vnd.dvb.iptv.alfec-base\":{\"source\":\"iana\"},\"application/vnd.dvb.iptv.alfec-enhancement\":{\"source\":\"iana\"},\"application/vnd.dvb.notif-aggregate-root+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dvb.notif-container+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dvb.notif-generic+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dvb.notif-ia-msglist+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dvb.notif-ia-registration-request+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dvb.notif-ia-registration-response+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dvb.notif-init+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.dvb.pfr\":{\"source\":\"iana\"},\"application/vnd.dvb.service\":{\"source\":\"iana\",\"extensions\":[\"svc\"]},\"application/vnd.dxr\":{\"source\":\"iana\"},\"application/vnd.dynageo\":{\"source\":\"iana\",\"extensions\":[\"geo\"]},\"application/vnd.dzr\":{\"source\":\"iana\"},\"application/vnd.easykaraoke.cdgdownload\":{\"source\":\"iana\"},\"application/vnd.ecdis-update\":{\"source\":\"iana\"},\"application/vnd.ecip.rlp\":{\"source\":\"iana\"},\"application/vnd.ecowin.chart\":{\"source\":\"iana\",\"extensions\":[\"mag\"]},\"application/vnd.ecowin.filerequest\":{\"source\":\"iana\"},\"application/vnd.ecowin.fileupdate\":{\"source\":\"iana\"},\"application/vnd.ecowin.series\":{\"source\":\"iana\"},\"application/vnd.ecowin.seriesrequest\":{\"source\":\"iana\"},\"application/vnd.ecowin.seriesupdate\":{\"source\":\"iana\"},\"application/vnd.efi.img\":{\"source\":\"iana\"},\"application/vnd.efi.iso\":{\"source\":\"iana\"},\"application/vnd.emclient.accessrequest+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.enliven\":{\"source\":\"iana\",\"extensions\":[\"nml\"]},\"application/vnd.enphase.envoy\":{\"source\":\"iana\"},\"application/vnd.eprints.data+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.epson.esf\":{\"source\":\"iana\",\"extensions\":[\"esf\"]},\"application/vnd.epson.msf\":{\"source\":\"iana\",\"extensions\":[\"msf\"]},\"application/vnd.epson.quickanime\":{\"source\":\"iana\",\"extensions\":[\"qam\"]},\"application/vnd.epson.salt\":{\"source\":\"iana\",\"extensions\":[\"slt\"]},\"application/vnd.epson.ssf\":{\"source\":\"iana\",\"extensions\":[\"ssf\"]},\"application/vnd.ericsson.quickcall\":{\"source\":\"iana\"},\"application/vnd.espass-espass+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.eszigno3+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"es3\",\"et3\"]},\"application/vnd.etsi.aoc+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.asic-e+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.etsi.asic-s+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.etsi.cug+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvcommand+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvdiscovery+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvprofile+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvsad-bc+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvsad-cod+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvsad-npvr+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvservice+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvsync+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.iptvueprofile+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.mcid+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.mheg5\":{\"source\":\"iana\"},\"application/vnd.etsi.overload-control-policy-dataset+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.pstn+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.sci+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.simservs+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.timestamp-token\":{\"source\":\"iana\"},\"application/vnd.etsi.tsl+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.etsi.tsl.der\":{\"source\":\"iana\"},\"application/vnd.eudora.data\":{\"source\":\"iana\"},\"application/vnd.evolv.ecig.profile\":{\"source\":\"iana\"},\"application/vnd.evolv.ecig.settings\":{\"source\":\"iana\"},\"application/vnd.evolv.ecig.theme\":{\"source\":\"iana\"},\"application/vnd.exstream-empower+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.exstream-package\":{\"source\":\"iana\"},\"application/vnd.ezpix-album\":{\"source\":\"iana\",\"extensions\":[\"ez2\"]},\"application/vnd.ezpix-package\":{\"source\":\"iana\",\"extensions\":[\"ez3\"]},\"application/vnd.f-secure.mobile\":{\"source\":\"iana\"},\"application/vnd.fastcopy-disk-image\":{\"source\":\"iana\"},\"application/vnd.fdf\":{\"source\":\"iana\",\"extensions\":[\"fdf\"]},\"application/vnd.fdsn.mseed\":{\"source\":\"iana\",\"extensions\":[\"mseed\"]},\"application/vnd.fdsn.seed\":{\"source\":\"iana\",\"extensions\":[\"seed\",\"dataless\"]},\"application/vnd.ffsns\":{\"source\":\"iana\"},\"application/vnd.ficlab.flb+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.filmit.zfc\":{\"source\":\"iana\"},\"application/vnd.fints\":{\"source\":\"iana\"},\"application/vnd.firemonkeys.cloudcell\":{\"source\":\"iana\"},\"application/vnd.flographit\":{\"source\":\"iana\",\"extensions\":[\"gph\"]},\"application/vnd.fluxtime.clip\":{\"source\":\"iana\",\"extensions\":[\"ftc\"]},\"application/vnd.font-fontforge-sfd\":{\"source\":\"iana\"},\"application/vnd.framemaker\":{\"source\":\"iana\",\"extensions\":[\"fm\",\"frame\",\"maker\",\"book\"]},\"application/vnd.frogans.fnc\":{\"source\":\"iana\",\"extensions\":[\"fnc\"]},\"application/vnd.frogans.ltf\":{\"source\":\"iana\",\"extensions\":[\"ltf\"]},\"application/vnd.fsc.weblaunch\":{\"source\":\"iana\",\"extensions\":[\"fsc\"]},\"application/vnd.fujitsu.oasys\":{\"source\":\"iana\",\"extensions\":[\"oas\"]},\"application/vnd.fujitsu.oasys2\":{\"source\":\"iana\",\"extensions\":[\"oa2\"]},\"application/vnd.fujitsu.oasys3\":{\"source\":\"iana\",\"extensions\":[\"oa3\"]},\"application/vnd.fujitsu.oasysgp\":{\"source\":\"iana\",\"extensions\":[\"fg5\"]},\"application/vnd.fujitsu.oasysprs\":{\"source\":\"iana\",\"extensions\":[\"bh2\"]},\"application/vnd.fujixerox.art-ex\":{\"source\":\"iana\"},\"application/vnd.fujixerox.art4\":{\"source\":\"iana\"},\"application/vnd.fujixerox.ddd\":{\"source\":\"iana\",\"extensions\":[\"ddd\"]},\"application/vnd.fujixerox.docuworks\":{\"source\":\"iana\",\"extensions\":[\"xdw\"]},\"application/vnd.fujixerox.docuworks.binder\":{\"source\":\"iana\",\"extensions\":[\"xbd\"]},\"application/vnd.fujixerox.docuworks.container\":{\"source\":\"iana\"},\"application/vnd.fujixerox.hbpl\":{\"source\":\"iana\"},\"application/vnd.fut-misnet\":{\"source\":\"iana\"},\"application/vnd.futoin+cbor\":{\"source\":\"iana\"},\"application/vnd.futoin+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.fuzzysheet\":{\"source\":\"iana\",\"extensions\":[\"fzs\"]},\"application/vnd.genomatix.tuxedo\":{\"source\":\"iana\",\"extensions\":[\"txd\"]},\"application/vnd.gentics.grd+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.geo+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.geocube+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.geogebra.file\":{\"source\":\"iana\",\"extensions\":[\"ggb\"]},\"application/vnd.geogebra.tool\":{\"source\":\"iana\",\"extensions\":[\"ggt\"]},\"application/vnd.geometry-explorer\":{\"source\":\"iana\",\"extensions\":[\"gex\",\"gre\"]},\"application/vnd.geonext\":{\"source\":\"iana\",\"extensions\":[\"gxt\"]},\"application/vnd.geoplan\":{\"source\":\"iana\",\"extensions\":[\"g2w\"]},\"application/vnd.geospace\":{\"source\":\"iana\",\"extensions\":[\"g3w\"]},\"application/vnd.gerber\":{\"source\":\"iana\"},\"application/vnd.globalplatform.card-content-mgt\":{\"source\":\"iana\"},\"application/vnd.globalplatform.card-content-mgt-response\":{\"source\":\"iana\"},\"application/vnd.gmx\":{\"source\":\"iana\",\"extensions\":[\"gmx\"]},\"application/vnd.google-apps.document\":{\"compressible\":false,\"extensions\":[\"gdoc\"]},\"application/vnd.google-apps.presentation\":{\"compressible\":false,\"extensions\":[\"gslides\"]},\"application/vnd.google-apps.spreadsheet\":{\"compressible\":false,\"extensions\":[\"gsheet\"]},\"application/vnd.google-earth.kml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"kml\"]},\"application/vnd.google-earth.kmz\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"kmz\"]},\"application/vnd.gov.sk.e-form+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.gov.sk.e-form+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.gov.sk.xmldatacontainer+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.grafeq\":{\"source\":\"iana\",\"extensions\":[\"gqf\",\"gqs\"]},\"application/vnd.gridmp\":{\"source\":\"iana\"},\"application/vnd.groove-account\":{\"source\":\"iana\",\"extensions\":[\"gac\"]},\"application/vnd.groove-help\":{\"source\":\"iana\",\"extensions\":[\"ghf\"]},\"application/vnd.groove-identity-message\":{\"source\":\"iana\",\"extensions\":[\"gim\"]},\"application/vnd.groove-injector\":{\"source\":\"iana\",\"extensions\":[\"grv\"]},\"application/vnd.groove-tool-message\":{\"source\":\"iana\",\"extensions\":[\"gtm\"]},\"application/vnd.groove-tool-template\":{\"source\":\"iana\",\"extensions\":[\"tpl\"]},\"application/vnd.groove-vcard\":{\"source\":\"iana\",\"extensions\":[\"vcg\"]},\"application/vnd.hal+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.hal+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"hal\"]},\"application/vnd.handheld-entertainment+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"zmm\"]},\"application/vnd.hbci\":{\"source\":\"iana\",\"extensions\":[\"hbci\"]},\"application/vnd.hc+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.hcl-bireports\":{\"source\":\"iana\"},\"application/vnd.hdt\":{\"source\":\"iana\"},\"application/vnd.heroku+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.hhe.lesson-player\":{\"source\":\"iana\",\"extensions\":[\"les\"]},\"application/vnd.hp-hpgl\":{\"source\":\"iana\",\"extensions\":[\"hpgl\"]},\"application/vnd.hp-hpid\":{\"source\":\"iana\",\"extensions\":[\"hpid\"]},\"application/vnd.hp-hps\":{\"source\":\"iana\",\"extensions\":[\"hps\"]},\"application/vnd.hp-jlyt\":{\"source\":\"iana\",\"extensions\":[\"jlt\"]},\"application/vnd.hp-pcl\":{\"source\":\"iana\",\"extensions\":[\"pcl\"]},\"application/vnd.hp-pclxl\":{\"source\":\"iana\",\"extensions\":[\"pclxl\"]},\"application/vnd.httphone\":{\"source\":\"iana\"},\"application/vnd.hydrostatix.sof-data\":{\"source\":\"iana\",\"extensions\":[\"sfd-hdstx\"]},\"application/vnd.hyper+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.hyper-item+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.hyperdrive+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.hzn-3d-crossword\":{\"source\":\"iana\"},\"application/vnd.ibm.afplinedata\":{\"source\":\"iana\"},\"application/vnd.ibm.electronic-media\":{\"source\":\"iana\"},\"application/vnd.ibm.minipay\":{\"source\":\"iana\",\"extensions\":[\"mpy\"]},\"application/vnd.ibm.modcap\":{\"source\":\"iana\",\"extensions\":[\"afp\",\"listafp\",\"list3820\"]},\"application/vnd.ibm.rights-management\":{\"source\":\"iana\",\"extensions\":[\"irm\"]},\"application/vnd.ibm.secure-container\":{\"source\":\"iana\",\"extensions\":[\"sc\"]},\"application/vnd.iccprofile\":{\"source\":\"iana\",\"extensions\":[\"icc\",\"icm\"]},\"application/vnd.ieee.1905\":{\"source\":\"iana\"},\"application/vnd.igloader\":{\"source\":\"iana\",\"extensions\":[\"igl\"]},\"application/vnd.imagemeter.folder+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.imagemeter.image+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.immervision-ivp\":{\"source\":\"iana\",\"extensions\":[\"ivp\"]},\"application/vnd.immervision-ivu\":{\"source\":\"iana\",\"extensions\":[\"ivu\"]},\"application/vnd.ims.imsccv1p1\":{\"source\":\"iana\"},\"application/vnd.ims.imsccv1p2\":{\"source\":\"iana\"},\"application/vnd.ims.imsccv1p3\":{\"source\":\"iana\"},\"application/vnd.ims.lis.v2.result+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ims.lti.v2.toolconsumerprofile+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ims.lti.v2.toolproxy+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ims.lti.v2.toolproxy.id+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ims.lti.v2.toolsettings+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ims.lti.v2.toolsettings.simple+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.informedcontrol.rms+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.informix-visionary\":{\"source\":\"iana\"},\"application/vnd.infotech.project\":{\"source\":\"iana\"},\"application/vnd.infotech.project+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.innopath.wamp.notification\":{\"source\":\"iana\"},\"application/vnd.insors.igm\":{\"source\":\"iana\",\"extensions\":[\"igm\"]},\"application/vnd.intercon.formnet\":{\"source\":\"iana\",\"extensions\":[\"xpw\",\"xpx\"]},\"application/vnd.intergeo\":{\"source\":\"iana\",\"extensions\":[\"i2g\"]},\"application/vnd.intertrust.digibox\":{\"source\":\"iana\"},\"application/vnd.intertrust.nncp\":{\"source\":\"iana\"},\"application/vnd.intu.qbo\":{\"source\":\"iana\",\"extensions\":[\"qbo\"]},\"application/vnd.intu.qfx\":{\"source\":\"iana\",\"extensions\":[\"qfx\"]},\"application/vnd.iptc.g2.catalogitem+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.iptc.g2.conceptitem+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.iptc.g2.knowledgeitem+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.iptc.g2.newsitem+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.iptc.g2.newsmessage+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.iptc.g2.packageitem+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.iptc.g2.planningitem+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ipunplugged.rcprofile\":{\"source\":\"iana\",\"extensions\":[\"rcprofile\"]},\"application/vnd.irepository.package+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"irp\"]},\"application/vnd.is-xpr\":{\"source\":\"iana\",\"extensions\":[\"xpr\"]},\"application/vnd.isac.fcs\":{\"source\":\"iana\",\"extensions\":[\"fcs\"]},\"application/vnd.iso11783-10+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.jam\":{\"source\":\"iana\",\"extensions\":[\"jam\"]},\"application/vnd.japannet-directory-service\":{\"source\":\"iana\"},\"application/vnd.japannet-jpnstore-wakeup\":{\"source\":\"iana\"},\"application/vnd.japannet-payment-wakeup\":{\"source\":\"iana\"},\"application/vnd.japannet-registration\":{\"source\":\"iana\"},\"application/vnd.japannet-registration-wakeup\":{\"source\":\"iana\"},\"application/vnd.japannet-setstore-wakeup\":{\"source\":\"iana\"},\"application/vnd.japannet-verification\":{\"source\":\"iana\"},\"application/vnd.japannet-verification-wakeup\":{\"source\":\"iana\"},\"application/vnd.jcp.javame.midlet-rms\":{\"source\":\"iana\",\"extensions\":[\"rms\"]},\"application/vnd.jisp\":{\"source\":\"iana\",\"extensions\":[\"jisp\"]},\"application/vnd.joost.joda-archive\":{\"source\":\"iana\",\"extensions\":[\"joda\"]},\"application/vnd.jsk.isdn-ngn\":{\"source\":\"iana\"},\"application/vnd.kahootz\":{\"source\":\"iana\",\"extensions\":[\"ktz\",\"ktr\"]},\"application/vnd.kde.karbon\":{\"source\":\"iana\",\"extensions\":[\"karbon\"]},\"application/vnd.kde.kchart\":{\"source\":\"iana\",\"extensions\":[\"chrt\"]},\"application/vnd.kde.kformula\":{\"source\":\"iana\",\"extensions\":[\"kfo\"]},\"application/vnd.kde.kivio\":{\"source\":\"iana\",\"extensions\":[\"flw\"]},\"application/vnd.kde.kontour\":{\"source\":\"iana\",\"extensions\":[\"kon\"]},\"application/vnd.kde.kpresenter\":{\"source\":\"iana\",\"extensions\":[\"kpr\",\"kpt\"]},\"application/vnd.kde.kspread\":{\"source\":\"iana\",\"extensions\":[\"ksp\"]},\"application/vnd.kde.kword\":{\"source\":\"iana\",\"extensions\":[\"kwd\",\"kwt\"]},\"application/vnd.kenameaapp\":{\"source\":\"iana\",\"extensions\":[\"htke\"]},\"application/vnd.kidspiration\":{\"source\":\"iana\",\"extensions\":[\"kia\"]},\"application/vnd.kinar\":{\"source\":\"iana\",\"extensions\":[\"kne\",\"knp\"]},\"application/vnd.koan\":{\"source\":\"iana\",\"extensions\":[\"skp\",\"skd\",\"skt\",\"skm\"]},\"application/vnd.kodak-descriptor\":{\"source\":\"iana\",\"extensions\":[\"sse\"]},\"application/vnd.las\":{\"source\":\"iana\"},\"application/vnd.las.las+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.las.las+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"lasxml\"]},\"application/vnd.laszip\":{\"source\":\"iana\"},\"application/vnd.leap+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.liberty-request+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.llamagraphics.life-balance.desktop\":{\"source\":\"iana\",\"extensions\":[\"lbd\"]},\"application/vnd.llamagraphics.life-balance.exchange+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"lbe\"]},\"application/vnd.logipipe.circuit+zip\":{\"source\":\"iana\",\"compressible\":false},\"application/vnd.loom\":{\"source\":\"iana\"},\"application/vnd.lotus-1-2-3\":{\"source\":\"iana\",\"extensions\":[\"123\"]},\"application/vnd.lotus-approach\":{\"source\":\"iana\",\"extensions\":[\"apr\"]},\"application/vnd.lotus-freelance\":{\"source\":\"iana\",\"extensions\":[\"pre\"]},\"application/vnd.lotus-notes\":{\"source\":\"iana\",\"extensions\":[\"nsf\"]},\"application/vnd.lotus-organizer\":{\"source\":\"iana\",\"extensions\":[\"org\"]},\"application/vnd.lotus-screencam\":{\"source\":\"iana\",\"extensions\":[\"scm\"]},\"application/vnd.lotus-wordpro\":{\"source\":\"iana\",\"extensions\":[\"lwp\"]},\"application/vnd.macports.portpkg\":{\"source\":\"iana\",\"extensions\":[\"portpkg\"]},\"application/vnd.mapbox-vector-tile\":{\"source\":\"iana\"},\"application/vnd.marlin.drm.actiontoken+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.marlin.drm.conftoken+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.marlin.drm.license+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.marlin.drm.mdcf\":{\"source\":\"iana\"},\"application/vnd.mason+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.maxmind.maxmind-db\":{\"source\":\"iana\"},\"application/vnd.mcd\":{\"source\":\"iana\",\"extensions\":[\"mcd\"]},\"application/vnd.medcalcdata\":{\"source\":\"iana\",\"extensions\":[\"mc1\"]},\"application/vnd.mediastation.cdkey\":{\"source\":\"iana\",\"extensions\":[\"cdkey\"]},\"application/vnd.meridian-slingshot\":{\"source\":\"iana\"},\"application/vnd.mfer\":{\"source\":\"iana\",\"extensions\":[\"mwf\"]},\"application/vnd.mfmp\":{\"source\":\"iana\",\"extensions\":[\"mfm\"]},\"application/vnd.micro+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.micrografx.flo\":{\"source\":\"iana\",\"extensions\":[\"flo\"]},\"application/vnd.micrografx.igx\":{\"source\":\"iana\",\"extensions\":[\"igx\"]},\"application/vnd.microsoft.portable-executable\":{\"source\":\"iana\"},\"application/vnd.microsoft.windows.thumbnail-cache\":{\"source\":\"iana\"},\"application/vnd.miele+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.mif\":{\"source\":\"iana\",\"extensions\":[\"mif\"]},\"application/vnd.minisoft-hp3000-save\":{\"source\":\"iana\"},\"application/vnd.mitsubishi.misty-guard.trustweb\":{\"source\":\"iana\"},\"application/vnd.mobius.daf\":{\"source\":\"iana\",\"extensions\":[\"daf\"]},\"application/vnd.mobius.dis\":{\"source\":\"iana\",\"extensions\":[\"dis\"]},\"application/vnd.mobius.mbk\":{\"source\":\"iana\",\"extensions\":[\"mbk\"]},\"application/vnd.mobius.mqy\":{\"source\":\"iana\",\"extensions\":[\"mqy\"]},\"application/vnd.mobius.msl\":{\"source\":\"iana\",\"extensions\":[\"msl\"]},\"application/vnd.mobius.plc\":{\"source\":\"iana\",\"extensions\":[\"plc\"]},\"application/vnd.mobius.txf\":{\"source\":\"iana\",\"extensions\":[\"txf\"]},\"application/vnd.mophun.application\":{\"source\":\"iana\",\"extensions\":[\"mpn\"]},\"application/vnd.mophun.certificate\":{\"source\":\"iana\",\"extensions\":[\"mpc\"]},\"application/vnd.motorola.flexsuite\":{\"source\":\"iana\"},\"application/vnd.motorola.flexsuite.adsi\":{\"source\":\"iana\"},\"application/vnd.motorola.flexsuite.fis\":{\"source\":\"iana\"},\"application/vnd.motorola.flexsuite.gotap\":{\"source\":\"iana\"},\"application/vnd.motorola.flexsuite.kmr\":{\"source\":\"iana\"},\"application/vnd.motorola.flexsuite.ttc\":{\"source\":\"iana\"},\"application/vnd.motorola.flexsuite.wem\":{\"source\":\"iana\"},\"application/vnd.motorola.iprm\":{\"source\":\"iana\"},\"application/vnd.mozilla.xul+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xul\"]},\"application/vnd.ms-3mfdocument\":{\"source\":\"iana\"},\"application/vnd.ms-artgalry\":{\"source\":\"iana\",\"extensions\":[\"cil\"]},\"application/vnd.ms-asf\":{\"source\":\"iana\"},\"application/vnd.ms-cab-compressed\":{\"source\":\"iana\",\"extensions\":[\"cab\"]},\"application/vnd.ms-color.iccprofile\":{\"source\":\"apache\"},\"application/vnd.ms-excel\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"xls\",\"xlm\",\"xla\",\"xlc\",\"xlt\",\"xlw\"]},\"application/vnd.ms-excel.addin.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"xlam\"]},\"application/vnd.ms-excel.sheet.binary.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"xlsb\"]},\"application/vnd.ms-excel.sheet.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"xlsm\"]},\"application/vnd.ms-excel.template.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"xltm\"]},\"application/vnd.ms-fontobject\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"eot\"]},\"application/vnd.ms-htmlhelp\":{\"source\":\"iana\",\"extensions\":[\"chm\"]},\"application/vnd.ms-ims\":{\"source\":\"iana\",\"extensions\":[\"ims\"]},\"application/vnd.ms-lrm\":{\"source\":\"iana\",\"extensions\":[\"lrm\"]},\"application/vnd.ms-office.activex+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ms-officetheme\":{\"source\":\"iana\",\"extensions\":[\"thmx\"]},\"application/vnd.ms-opentype\":{\"source\":\"apache\",\"compressible\":true},\"application/vnd.ms-outlook\":{\"compressible\":false,\"extensions\":[\"msg\"]},\"application/vnd.ms-package.obfuscated-opentype\":{\"source\":\"apache\"},\"application/vnd.ms-pki.seccat\":{\"source\":\"apache\",\"extensions\":[\"cat\"]},\"application/vnd.ms-pki.stl\":{\"source\":\"apache\",\"extensions\":[\"stl\"]},\"application/vnd.ms-playready.initiator+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ms-powerpoint\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"ppt\",\"pps\",\"pot\"]},\"application/vnd.ms-powerpoint.addin.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"ppam\"]},\"application/vnd.ms-powerpoint.presentation.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"pptm\"]},\"application/vnd.ms-powerpoint.slide.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"sldm\"]},\"application/vnd.ms-powerpoint.slideshow.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"ppsm\"]},\"application/vnd.ms-powerpoint.template.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"potm\"]},\"application/vnd.ms-printdevicecapabilities+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ms-printing.printticket+xml\":{\"source\":\"apache\",\"compressible\":true},\"application/vnd.ms-printschematicket+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.ms-project\":{\"source\":\"iana\",\"extensions\":[\"mpp\",\"mpt\"]},\"application/vnd.ms-tnef\":{\"source\":\"iana\"},\"application/vnd.ms-windows.devicepairing\":{\"source\":\"iana\"},\"application/vnd.ms-windows.nwprinting.oob\":{\"source\":\"iana\"},\"application/vnd.ms-windows.printerpairing\":{\"source\":\"iana\"},\"application/vnd.ms-windows.wsd.oob\":{\"source\":\"iana\"},\"application/vnd.ms-wmdrm.lic-chlg-req\":{\"source\":\"iana\"},\"application/vnd.ms-wmdrm.lic-resp\":{\"source\":\"iana\"},\"application/vnd.ms-wmdrm.meter-chlg-req\":{\"source\":\"iana\"},\"application/vnd.ms-wmdrm.meter-resp\":{\"source\":\"iana\"},\"application/vnd.ms-word.document.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"docm\"]},\"application/vnd.ms-word.template.macroenabled.12\":{\"source\":\"iana\",\"extensions\":[\"dotm\"]},\"application/vnd.ms-works\":{\"source\":\"iana\",\"extensions\":[\"wps\",\"wks\",\"wcm\",\"wdb\"]},\"application/vnd.ms-wpl\":{\"source\":\"iana\",\"extensions\":[\"wpl\"]},\"application/vnd.ms-xpsdocument\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"xps\"]},\"application/vnd.msa-disk-image\":{\"source\":\"iana\"},\"application/vnd.mseq\":{\"source\":\"iana\",\"extensions\":[\"mseq\"]},\"application/vnd.msign\":{\"source\":\"iana\"},\"application/vnd.multiad.creator\":{\"source\":\"iana\"},\"application/vnd.multiad.creator.cif\":{\"source\":\"iana\"},\"application/vnd.music-niff\":{\"source\":\"iana\"},\"application/vnd.musician\":{\"source\":\"iana\",\"extensions\":[\"mus\"]},\"application/vnd.muvee.style\":{\"source\":\"iana\",\"extensions\":[\"msty\"]},\"application/vnd.mynfc\":{\"source\":\"iana\",\"extensions\":[\"taglet\"]},\"application/vnd.ncd.control\":{\"source\":\"iana\"},\"application/vnd.ncd.reference\":{\"source\":\"iana\"},\"application/vnd.nearst.inv+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.nervana\":{\"source\":\"iana\"},\"application/vnd.netfpx\":{\"source\":\"iana\"},\"application/vnd.neurolanguage.nlu\":{\"source\":\"iana\",\"extensions\":[\"nlu\"]},\"application/vnd.nimn\":{\"source\":\"iana\"},\"application/vnd.nintendo.nitro.rom\":{\"source\":\"iana\"},\"application/vnd.nintendo.snes.rom\":{\"source\":\"iana\"},\"application/vnd.nitf\":{\"source\":\"iana\",\"extensions\":[\"ntf\",\"nitf\"]},\"application/vnd.noblenet-directory\":{\"source\":\"iana\",\"extensions\":[\"nnd\"]},\"application/vnd.noblenet-sealer\":{\"source\":\"iana\",\"extensions\":[\"nns\"]},\"application/vnd.noblenet-web\":{\"source\":\"iana\",\"extensions\":[\"nnw\"]},\"application/vnd.nokia.catalogs\":{\"source\":\"iana\"},\"application/vnd.nokia.conml+wbxml\":{\"source\":\"iana\"},\"application/vnd.nokia.conml+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.nokia.iptv.config+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.nokia.isds-radio-presets\":{\"source\":\"iana\"},\"application/vnd.nokia.landmark+wbxml\":{\"source\":\"iana\"},\"application/vnd.nokia.landmark+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.nokia.landmarkcollection+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.nokia.n-gage.ac+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"ac\"]},\"application/vnd.nokia.n-gage.data\":{\"source\":\"iana\",\"extensions\":[\"ngdat\"]},\"application/vnd.nokia.n-gage.symbian.install\":{\"source\":\"iana\",\"extensions\":[\"n-gage\"]},\"application/vnd.nokia.ncd\":{\"source\":\"iana\"},\"application/vnd.nokia.pcd+wbxml\":{\"source\":\"iana\"},\"application/vnd.nokia.pcd+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.nokia.radio-preset\":{\"source\":\"iana\",\"extensions\":[\"rpst\"]},\"application/vnd.nokia.radio-presets\":{\"source\":\"iana\",\"extensions\":[\"rpss\"]},\"application/vnd.novadigm.edm\":{\"source\":\"iana\",\"extensions\":[\"edm\"]},\"application/vnd.novadigm.edx\":{\"source\":\"iana\",\"extensions\":[\"edx\"]},\"application/vnd.novadigm.ext\":{\"source\":\"iana\",\"extensions\":[\"ext\"]},\"application/vnd.ntt-local.content-share\":{\"source\":\"iana\"},\"application/vnd.ntt-local.file-transfer\":{\"source\":\"iana\"},\"application/vnd.ntt-local.ogw_remote-access\":{\"source\":\"iana\"},\"application/vnd.ntt-local.sip-ta_remote\":{\"source\":\"iana\"},\"application/vnd.ntt-local.sip-ta_tcp_stream\":{\"source\":\"iana\"},\"application/vnd.oasis.opendocument.chart\":{\"source\":\"iana\",\"extensions\":[\"odc\"]},\"application/vnd.oasis.opendocument.chart-template\":{\"source\":\"iana\",\"extensions\":[\"otc\"]},\"application/vnd.oasis.opendocument.database\":{\"source\":\"iana\",\"extensions\":[\"odb\"]},\"application/vnd.oasis.opendocument.formula\":{\"source\":\"iana\",\"extensions\":[\"odf\"]},\"application/vnd.oasis.opendocument.formula-template\":{\"source\":\"iana\",\"extensions\":[\"odft\"]},\"application/vnd.oasis.opendocument.graphics\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"odg\"]},\"application/vnd.oasis.opendocument.graphics-template\":{\"source\":\"iana\",\"extensions\":[\"otg\"]},\"application/vnd.oasis.opendocument.image\":{\"source\":\"iana\",\"extensions\":[\"odi\"]},\"application/vnd.oasis.opendocument.image-template\":{\"source\":\"iana\",\"extensions\":[\"oti\"]},\"application/vnd.oasis.opendocument.presentation\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"odp\"]},\"application/vnd.oasis.opendocument.presentation-template\":{\"source\":\"iana\",\"extensions\":[\"otp\"]},\"application/vnd.oasis.opendocument.spreadsheet\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"ods\"]},\"application/vnd.oasis.opendocument.spreadsheet-template\":{\"source\":\"iana\",\"extensions\":[\"ots\"]},\"application/vnd.oasis.opendocument.text\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"odt\"]},\"application/vnd.oasis.opendocument.text-master\":{\"source\":\"iana\",\"extensions\":[\"odm\"]},\"application/vnd.oasis.opendocument.text-template\":{\"source\":\"iana\",\"extensions\":[\"ott\"]},\"application/vnd.oasis.opendocument.text-web\":{\"source\":\"iana\",\"extensions\":[\"oth\"]},\"application/vnd.obn\":{\"source\":\"iana\"},\"application/vnd.ocf+cbor\":{\"source\":\"iana\"},\"application/vnd.oci.image.manifest.v1+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oftn.l10n+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.contentaccessdownload+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.contentaccessstreaming+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.cspg-hexbinary\":{\"source\":\"iana\"},\"application/vnd.oipf.dae.svg+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.dae.xhtml+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.mippvcontrolmessage+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.pae.gem\":{\"source\":\"iana\"},\"application/vnd.oipf.spdiscovery+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.spdlist+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.ueprofile+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oipf.userprofile+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.olpc-sugar\":{\"source\":\"iana\",\"extensions\":[\"xo\"]},\"application/vnd.oma-scws-config\":{\"source\":\"iana\"},\"application/vnd.oma-scws-http-request\":{\"source\":\"iana\"},\"application/vnd.oma-scws-http-response\":{\"source\":\"iana\"},\"application/vnd.oma.bcast.associated-procedure-parameter+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.bcast.drm-trigger+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.bcast.imd+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.bcast.ltkm\":{\"source\":\"iana\"},\"application/vnd.oma.bcast.notification+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.bcast.provisioningtrigger\":{\"source\":\"iana\"},\"application/vnd.oma.bcast.sgboot\":{\"source\":\"iana\"},\"application/vnd.oma.bcast.sgdd+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.bcast.sgdu\":{\"source\":\"iana\"},\"application/vnd.oma.bcast.simple-symbol-container\":{\"source\":\"iana\"},\"application/vnd.oma.bcast.smartcard-trigger+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.bcast.sprov+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.bcast.stkm\":{\"source\":\"iana\"},\"application/vnd.oma.cab-address-book+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.cab-feature-handler+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.cab-pcc+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.cab-subs-invite+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.cab-user-prefs+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.dcd\":{\"source\":\"iana\"},\"application/vnd.oma.dcdc\":{\"source\":\"iana\"},\"application/vnd.oma.dd2+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"dd2\"]},\"application/vnd.oma.drm.risd+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.group-usage-list+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.lwm2m+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.lwm2m+tlv\":{\"source\":\"iana\"},\"application/vnd.oma.pal+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.poc.detailed-progress-report+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.poc.final-report+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.poc.groups+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.poc.invocation-descriptor+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.poc.optimized-progress-report+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.push\":{\"source\":\"iana\"},\"application/vnd.oma.scidm.messages+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oma.xcap-directory+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.omads-email+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/vnd.omads-file+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/vnd.omads-folder+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/vnd.omaloc-supl-init\":{\"source\":\"iana\"},\"application/vnd.onepager\":{\"source\":\"iana\"},\"application/vnd.onepagertamp\":{\"source\":\"iana\"},\"application/vnd.onepagertamx\":{\"source\":\"iana\"},\"application/vnd.onepagertat\":{\"source\":\"iana\"},\"application/vnd.onepagertatp\":{\"source\":\"iana\"},\"application/vnd.onepagertatx\":{\"source\":\"iana\"},\"application/vnd.openblox.game+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"obgx\"]},\"application/vnd.openblox.game-binary\":{\"source\":\"iana\"},\"application/vnd.openeye.oeb\":{\"source\":\"iana\"},\"application/vnd.openofficeorg.extension\":{\"source\":\"apache\",\"extensions\":[\"oxt\"]},\"application/vnd.openstreetmap.data+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"osm\"]},\"application/vnd.openxmlformats-officedocument.custom-properties+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.customxmlproperties+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.drawing+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.drawingml.chart+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.extended-properties+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.comments+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.presentation\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"pptx\"]},\"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.presprops+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.slide\":{\"source\":\"iana\",\"extensions\":[\"sldx\"]},\"application/vnd.openxmlformats-officedocument.presentationml.slide+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.slideshow\":{\"source\":\"iana\",\"extensions\":[\"ppsx\"]},\"application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.tags+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.template\":{\"source\":\"iana\",\"extensions\":[\"potx\"]},\"application/vnd.openxmlformats-officedocument.presentationml.template.main+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"xlsx\"]},\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.template\":{\"source\":\"iana\",\"extensions\":[\"xltx\"]},\"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.theme+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.themeoverride+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.vmldrawing\":{\"source\":\"iana\"},\"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"docx\"]},\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.template\":{\"source\":\"iana\",\"extensions\":[\"dotx\"]},\"application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-package.core-properties+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.openxmlformats-package.relationships+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oracle.resource+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.orange.indata\":{\"source\":\"iana\"},\"application/vnd.osa.netdeploy\":{\"source\":\"iana\"},\"application/vnd.osgeo.mapguide.package\":{\"source\":\"iana\",\"extensions\":[\"mgp\"]},\"application/vnd.osgi.bundle\":{\"source\":\"iana\"},\"application/vnd.osgi.dp\":{\"source\":\"iana\",\"extensions\":[\"dp\"]},\"application/vnd.osgi.subsystem\":{\"source\":\"iana\",\"extensions\":[\"esa\"]},\"application/vnd.otps.ct-kip+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.oxli.countgraph\":{\"source\":\"iana\"},\"application/vnd.pagerduty+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.palm\":{\"source\":\"iana\",\"extensions\":[\"pdb\",\"pqa\",\"oprc\"]},\"application/vnd.panoply\":{\"source\":\"iana\"},\"application/vnd.paos.xml\":{\"source\":\"iana\"},\"application/vnd.patentdive\":{\"source\":\"iana\"},\"application/vnd.patientecommsdoc\":{\"source\":\"iana\"},\"application/vnd.pawaafile\":{\"source\":\"iana\",\"extensions\":[\"paw\"]},\"application/vnd.pcos\":{\"source\":\"iana\"},\"application/vnd.pg.format\":{\"source\":\"iana\",\"extensions\":[\"str\"]},\"application/vnd.pg.osasli\":{\"source\":\"iana\",\"extensions\":[\"ei6\"]},\"application/vnd.piaccess.application-licence\":{\"source\":\"iana\"},\"application/vnd.picsel\":{\"source\":\"iana\",\"extensions\":[\"efif\"]},\"application/vnd.pmi.widget\":{\"source\":\"iana\",\"extensions\":[\"wg\"]},\"application/vnd.poc.group-advertisement+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.pocketlearn\":{\"source\":\"iana\",\"extensions\":[\"plf\"]},\"application/vnd.powerbuilder6\":{\"source\":\"iana\",\"extensions\":[\"pbd\"]},\"application/vnd.powerbuilder6-s\":{\"source\":\"iana\"},\"application/vnd.powerbuilder7\":{\"source\":\"iana\"},\"application/vnd.powerbuilder7-s\":{\"source\":\"iana\"},\"application/vnd.powerbuilder75\":{\"source\":\"iana\"},\"application/vnd.powerbuilder75-s\":{\"source\":\"iana\"},\"application/vnd.preminet\":{\"source\":\"iana\"},\"application/vnd.previewsystems.box\":{\"source\":\"iana\",\"extensions\":[\"box\"]},\"application/vnd.proteus.magazine\":{\"source\":\"iana\",\"extensions\":[\"mgz\"]},\"application/vnd.psfs\":{\"source\":\"iana\"},\"application/vnd.publishare-delta-tree\":{\"source\":\"iana\",\"extensions\":[\"qps\"]},\"application/vnd.pvi.ptid1\":{\"source\":\"iana\",\"extensions\":[\"ptid\"]},\"application/vnd.pwg-multiplexed\":{\"source\":\"iana\"},\"application/vnd.pwg-xhtml-print+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.qualcomm.brew-app-res\":{\"source\":\"iana\"},\"application/vnd.quarantainenet\":{\"source\":\"iana\"},\"application/vnd.quark.quarkxpress\":{\"source\":\"iana\",\"extensions\":[\"qxd\",\"qxt\",\"qwd\",\"qwt\",\"qxl\",\"qxb\"]},\"application/vnd.quobject-quoxdocument\":{\"source\":\"iana\"},\"application/vnd.radisys.moml+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-audit+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-audit-conf+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-audit-conn+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-audit-dialog+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-audit-stream+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-conf+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-dialog+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-dialog-base+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-dialog-fax-detect+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-dialog-fax-sendrecv+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-dialog-group+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-dialog-speech+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.radisys.msml-dialog-transform+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.rainstor.data\":{\"source\":\"iana\"},\"application/vnd.rapid\":{\"source\":\"iana\"},\"application/vnd.rar\":{\"source\":\"iana\"},\"application/vnd.realvnc.bed\":{\"source\":\"iana\",\"extensions\":[\"bed\"]},\"application/vnd.recordare.musicxml\":{\"source\":\"iana\",\"extensions\":[\"mxl\"]},\"application/vnd.recordare.musicxml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"musicxml\"]},\"application/vnd.renlearn.rlprint\":{\"source\":\"iana\"},\"application/vnd.restful+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.rig.cryptonote\":{\"source\":\"iana\",\"extensions\":[\"cryptonote\"]},\"application/vnd.rim.cod\":{\"source\":\"apache\",\"extensions\":[\"cod\"]},\"application/vnd.rn-realmedia\":{\"source\":\"apache\",\"extensions\":[\"rm\"]},\"application/vnd.rn-realmedia-vbr\":{\"source\":\"apache\",\"extensions\":[\"rmvb\"]},\"application/vnd.route66.link66+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"link66\"]},\"application/vnd.rs-274x\":{\"source\":\"iana\"},\"application/vnd.ruckus.download\":{\"source\":\"iana\"},\"application/vnd.s3sms\":{\"source\":\"iana\"},\"application/vnd.sailingtracker.track\":{\"source\":\"iana\",\"extensions\":[\"st\"]},\"application/vnd.sar\":{\"source\":\"iana\"},\"application/vnd.sbm.cid\":{\"source\":\"iana\"},\"application/vnd.sbm.mid2\":{\"source\":\"iana\"},\"application/vnd.scribus\":{\"source\":\"iana\"},\"application/vnd.sealed.3df\":{\"source\":\"iana\"},\"application/vnd.sealed.csf\":{\"source\":\"iana\"},\"application/vnd.sealed.doc\":{\"source\":\"iana\"},\"application/vnd.sealed.eml\":{\"source\":\"iana\"},\"application/vnd.sealed.mht\":{\"source\":\"iana\"},\"application/vnd.sealed.net\":{\"source\":\"iana\"},\"application/vnd.sealed.ppt\":{\"source\":\"iana\"},\"application/vnd.sealed.tiff\":{\"source\":\"iana\"},\"application/vnd.sealed.xls\":{\"source\":\"iana\"},\"application/vnd.sealedmedia.softseal.html\":{\"source\":\"iana\"},\"application/vnd.sealedmedia.softseal.pdf\":{\"source\":\"iana\"},\"application/vnd.seemail\":{\"source\":\"iana\",\"extensions\":[\"see\"]},\"application/vnd.sema\":{\"source\":\"iana\",\"extensions\":[\"sema\"]},\"application/vnd.semd\":{\"source\":\"iana\",\"extensions\":[\"semd\"]},\"application/vnd.semf\":{\"source\":\"iana\",\"extensions\":[\"semf\"]},\"application/vnd.shade-save-file\":{\"source\":\"iana\"},\"application/vnd.shana.informed.formdata\":{\"source\":\"iana\",\"extensions\":[\"ifm\"]},\"application/vnd.shana.informed.formtemplate\":{\"source\":\"iana\",\"extensions\":[\"itp\"]},\"application/vnd.shana.informed.interchange\":{\"source\":\"iana\",\"extensions\":[\"iif\"]},\"application/vnd.shana.informed.package\":{\"source\":\"iana\",\"extensions\":[\"ipk\"]},\"application/vnd.shootproof+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.shopkick+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.shp\":{\"source\":\"iana\"},\"application/vnd.shx\":{\"source\":\"iana\"},\"application/vnd.sigrok.session\":{\"source\":\"iana\"},\"application/vnd.simtech-mindmapper\":{\"source\":\"iana\",\"extensions\":[\"twd\",\"twds\"]},\"application/vnd.siren+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.smaf\":{\"source\":\"iana\",\"extensions\":[\"mmf\"]},\"application/vnd.smart.notebook\":{\"source\":\"iana\"},\"application/vnd.smart.teacher\":{\"source\":\"iana\",\"extensions\":[\"teacher\"]},\"application/vnd.snesdev-page-table\":{\"source\":\"iana\"},\"application/vnd.software602.filler.form+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"fo\"]},\"application/vnd.software602.filler.form-xml-zip\":{\"source\":\"iana\"},\"application/vnd.solent.sdkm+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"sdkm\",\"sdkd\"]},\"application/vnd.spotfire.dxp\":{\"source\":\"iana\",\"extensions\":[\"dxp\"]},\"application/vnd.spotfire.sfs\":{\"source\":\"iana\",\"extensions\":[\"sfs\"]},\"application/vnd.sqlite3\":{\"source\":\"iana\"},\"application/vnd.sss-cod\":{\"source\":\"iana\"},\"application/vnd.sss-dtf\":{\"source\":\"iana\"},\"application/vnd.sss-ntf\":{\"source\":\"iana\"},\"application/vnd.stardivision.calc\":{\"source\":\"apache\",\"extensions\":[\"sdc\"]},\"application/vnd.stardivision.draw\":{\"source\":\"apache\",\"extensions\":[\"sda\"]},\"application/vnd.stardivision.impress\":{\"source\":\"apache\",\"extensions\":[\"sdd\"]},\"application/vnd.stardivision.math\":{\"source\":\"apache\",\"extensions\":[\"smf\"]},\"application/vnd.stardivision.writer\":{\"source\":\"apache\",\"extensions\":[\"sdw\",\"vor\"]},\"application/vnd.stardivision.writer-global\":{\"source\":\"apache\",\"extensions\":[\"sgl\"]},\"application/vnd.stepmania.package\":{\"source\":\"iana\",\"extensions\":[\"smzip\"]},\"application/vnd.stepmania.stepchart\":{\"source\":\"iana\",\"extensions\":[\"sm\"]},\"application/vnd.street-stream\":{\"source\":\"iana\"},\"application/vnd.sun.wadl+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"wadl\"]},\"application/vnd.sun.xml.calc\":{\"source\":\"apache\",\"extensions\":[\"sxc\"]},\"application/vnd.sun.xml.calc.template\":{\"source\":\"apache\",\"extensions\":[\"stc\"]},\"application/vnd.sun.xml.draw\":{\"source\":\"apache\",\"extensions\":[\"sxd\"]},\"application/vnd.sun.xml.draw.template\":{\"source\":\"apache\",\"extensions\":[\"std\"]},\"application/vnd.sun.xml.impress\":{\"source\":\"apache\",\"extensions\":[\"sxi\"]},\"application/vnd.sun.xml.impress.template\":{\"source\":\"apache\",\"extensions\":[\"sti\"]},\"application/vnd.sun.xml.math\":{\"source\":\"apache\",\"extensions\":[\"sxm\"]},\"application/vnd.sun.xml.writer\":{\"source\":\"apache\",\"extensions\":[\"sxw\"]},\"application/vnd.sun.xml.writer.global\":{\"source\":\"apache\",\"extensions\":[\"sxg\"]},\"application/vnd.sun.xml.writer.template\":{\"source\":\"apache\",\"extensions\":[\"stw\"]},\"application/vnd.sus-calendar\":{\"source\":\"iana\",\"extensions\":[\"sus\",\"susp\"]},\"application/vnd.svd\":{\"source\":\"iana\",\"extensions\":[\"svd\"]},\"application/vnd.swiftview-ics\":{\"source\":\"iana\"},\"application/vnd.symbian.install\":{\"source\":\"apache\",\"extensions\":[\"sis\",\"sisx\"]},\"application/vnd.syncml+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"xsm\"]},\"application/vnd.syncml.dm+wbxml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"extensions\":[\"bdm\"]},\"application/vnd.syncml.dm+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"xdm\"]},\"application/vnd.syncml.dm.notification\":{\"source\":\"iana\"},\"application/vnd.syncml.dmddf+wbxml\":{\"source\":\"iana\"},\"application/vnd.syncml.dmddf+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"ddf\"]},\"application/vnd.syncml.dmtnds+wbxml\":{\"source\":\"iana\"},\"application/vnd.syncml.dmtnds+xml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true},\"application/vnd.syncml.ds.notification\":{\"source\":\"iana\"},\"application/vnd.tableschema+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.tao.intent-module-archive\":{\"source\":\"iana\",\"extensions\":[\"tao\"]},\"application/vnd.tcpdump.pcap\":{\"source\":\"iana\",\"extensions\":[\"pcap\",\"cap\",\"dmp\"]},\"application/vnd.think-cell.ppttc+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.tmd.mediaflex.api+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.tml\":{\"source\":\"iana\"},\"application/vnd.tmobile-livetv\":{\"source\":\"iana\",\"extensions\":[\"tmo\"]},\"application/vnd.tri.onesource\":{\"source\":\"iana\"},\"application/vnd.trid.tpt\":{\"source\":\"iana\",\"extensions\":[\"tpt\"]},\"application/vnd.triscape.mxs\":{\"source\":\"iana\",\"extensions\":[\"mxs\"]},\"application/vnd.trueapp\":{\"source\":\"iana\",\"extensions\":[\"tra\"]},\"application/vnd.truedoc\":{\"source\":\"iana\"},\"application/vnd.ubisoft.webplayer\":{\"source\":\"iana\"},\"application/vnd.ufdl\":{\"source\":\"iana\",\"extensions\":[\"ufd\",\"ufdl\"]},\"application/vnd.uiq.theme\":{\"source\":\"iana\",\"extensions\":[\"utz\"]},\"application/vnd.umajin\":{\"source\":\"iana\",\"extensions\":[\"umj\"]},\"application/vnd.unity\":{\"source\":\"iana\",\"extensions\":[\"unityweb\"]},\"application/vnd.uoml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"uoml\"]},\"application/vnd.uplanet.alert\":{\"source\":\"iana\"},\"application/vnd.uplanet.alert-wbxml\":{\"source\":\"iana\"},\"application/vnd.uplanet.bearer-choice\":{\"source\":\"iana\"},\"application/vnd.uplanet.bearer-choice-wbxml\":{\"source\":\"iana\"},\"application/vnd.uplanet.cacheop\":{\"source\":\"iana\"},\"application/vnd.uplanet.cacheop-wbxml\":{\"source\":\"iana\"},\"application/vnd.uplanet.channel\":{\"source\":\"iana\"},\"application/vnd.uplanet.channel-wbxml\":{\"source\":\"iana\"},\"application/vnd.uplanet.list\":{\"source\":\"iana\"},\"application/vnd.uplanet.list-wbxml\":{\"source\":\"iana\"},\"application/vnd.uplanet.listcmd\":{\"source\":\"iana\"},\"application/vnd.uplanet.listcmd-wbxml\":{\"source\":\"iana\"},\"application/vnd.uplanet.signal\":{\"source\":\"iana\"},\"application/vnd.uri-map\":{\"source\":\"iana\"},\"application/vnd.valve.source.material\":{\"source\":\"iana\"},\"application/vnd.vcx\":{\"source\":\"iana\",\"extensions\":[\"vcx\"]},\"application/vnd.vd-study\":{\"source\":\"iana\"},\"application/vnd.vectorworks\":{\"source\":\"iana\"},\"application/vnd.vel+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.verimatrix.vcas\":{\"source\":\"iana\"},\"application/vnd.veryant.thin\":{\"source\":\"iana\"},\"application/vnd.ves.encrypted\":{\"source\":\"iana\"},\"application/vnd.vidsoft.vidconference\":{\"source\":\"iana\"},\"application/vnd.visio\":{\"source\":\"iana\",\"extensions\":[\"vsd\",\"vst\",\"vss\",\"vsw\"]},\"application/vnd.visionary\":{\"source\":\"iana\",\"extensions\":[\"vis\"]},\"application/vnd.vividence.scriptfile\":{\"source\":\"iana\"},\"application/vnd.vsf\":{\"source\":\"iana\",\"extensions\":[\"vsf\"]},\"application/vnd.wap.sic\":{\"source\":\"iana\"},\"application/vnd.wap.slc\":{\"source\":\"iana\"},\"application/vnd.wap.wbxml\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"extensions\":[\"wbxml\"]},\"application/vnd.wap.wmlc\":{\"source\":\"iana\",\"extensions\":[\"wmlc\"]},\"application/vnd.wap.wmlscriptc\":{\"source\":\"iana\",\"extensions\":[\"wmlsc\"]},\"application/vnd.webturbo\":{\"source\":\"iana\",\"extensions\":[\"wtb\"]},\"application/vnd.wfa.p2p\":{\"source\":\"iana\"},\"application/vnd.wfa.wsc\":{\"source\":\"iana\"},\"application/vnd.windows.devicepairing\":{\"source\":\"iana\"},\"application/vnd.wmc\":{\"source\":\"iana\"},\"application/vnd.wmf.bootstrap\":{\"source\":\"iana\"},\"application/vnd.wolfram.mathematica\":{\"source\":\"iana\"},\"application/vnd.wolfram.mathematica.package\":{\"source\":\"iana\"},\"application/vnd.wolfram.player\":{\"source\":\"iana\",\"extensions\":[\"nbp\"]},\"application/vnd.wordperfect\":{\"source\":\"iana\",\"extensions\":[\"wpd\"]},\"application/vnd.wqd\":{\"source\":\"iana\",\"extensions\":[\"wqd\"]},\"application/vnd.wrq-hp3000-labelled\":{\"source\":\"iana\"},\"application/vnd.wt.stf\":{\"source\":\"iana\",\"extensions\":[\"stf\"]},\"application/vnd.wv.csp+wbxml\":{\"source\":\"iana\"},\"application/vnd.wv.csp+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.wv.ssp+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.xacml+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.xara\":{\"source\":\"iana\",\"extensions\":[\"xar\"]},\"application/vnd.xfdl\":{\"source\":\"iana\",\"extensions\":[\"xfdl\"]},\"application/vnd.xfdl.webform\":{\"source\":\"iana\"},\"application/vnd.xmi+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/vnd.xmpie.cpkg\":{\"source\":\"iana\"},\"application/vnd.xmpie.dpkg\":{\"source\":\"iana\"},\"application/vnd.xmpie.plan\":{\"source\":\"iana\"},\"application/vnd.xmpie.ppkg\":{\"source\":\"iana\"},\"application/vnd.xmpie.xlim\":{\"source\":\"iana\"},\"application/vnd.yamaha.hv-dic\":{\"source\":\"iana\",\"extensions\":[\"hvd\"]},\"application/vnd.yamaha.hv-script\":{\"source\":\"iana\",\"extensions\":[\"hvs\"]},\"application/vnd.yamaha.hv-voice\":{\"source\":\"iana\",\"extensions\":[\"hvp\"]},\"application/vnd.yamaha.openscoreformat\":{\"source\":\"iana\",\"extensions\":[\"osf\"]},\"application/vnd.yamaha.openscoreformat.osfpvg+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"osfpvg\"]},\"application/vnd.yamaha.remote-setup\":{\"source\":\"iana\"},\"application/vnd.yamaha.smaf-audio\":{\"source\":\"iana\",\"extensions\":[\"saf\"]},\"application/vnd.yamaha.smaf-phrase\":{\"source\":\"iana\",\"extensions\":[\"spf\"]},\"application/vnd.yamaha.through-ngn\":{\"source\":\"iana\"},\"application/vnd.yamaha.tunnel-udpencap\":{\"source\":\"iana\"},\"application/vnd.yaoweme\":{\"source\":\"iana\"},\"application/vnd.yellowriver-custom-menu\":{\"source\":\"iana\",\"extensions\":[\"cmp\"]},\"application/vnd.youtube.yt\":{\"source\":\"iana\"},\"application/vnd.zul\":{\"source\":\"iana\",\"extensions\":[\"zir\",\"zirz\"]},\"application/vnd.zzazz.deck+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"zaz\"]},\"application/voicexml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"vxml\"]},\"application/voucher-cms+json\":{\"source\":\"iana\",\"compressible\":true},\"application/vq-rtcpxr\":{\"source\":\"iana\"},\"application/wasm\":{\"compressible\":true,\"extensions\":[\"wasm\"]},\"application/watcherinfo+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/webpush-options+json\":{\"source\":\"iana\",\"compressible\":true},\"application/whoispp-query\":{\"source\":\"iana\"},\"application/whoispp-response\":{\"source\":\"iana\"},\"application/widget\":{\"source\":\"iana\",\"extensions\":[\"wgt\"]},\"application/winhlp\":{\"source\":\"apache\",\"extensions\":[\"hlp\"]},\"application/wita\":{\"source\":\"iana\"},\"application/wordperfect5.1\":{\"source\":\"iana\"},\"application/wsdl+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"wsdl\"]},\"application/wspolicy+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"wspolicy\"]},\"application/x-7z-compressed\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"7z\"]},\"application/x-abiword\":{\"source\":\"apache\",\"extensions\":[\"abw\"]},\"application/x-ace-compressed\":{\"source\":\"apache\",\"extensions\":[\"ace\"]},\"application/x-amf\":{\"source\":\"apache\"},\"application/x-apple-diskimage\":{\"source\":\"apache\",\"extensions\":[\"dmg\"]},\"application/x-arj\":{\"compressible\":false,\"extensions\":[\"arj\"]},\"application/x-authorware-bin\":{\"source\":\"apache\",\"extensions\":[\"aab\",\"x32\",\"u32\",\"vox\"]},\"application/x-authorware-map\":{\"source\":\"apache\",\"extensions\":[\"aam\"]},\"application/x-authorware-seg\":{\"source\":\"apache\",\"extensions\":[\"aas\"]},\"application/x-bcpio\":{\"source\":\"apache\",\"extensions\":[\"bcpio\"]},\"application/x-bdoc\":{\"compressible\":false,\"extensions\":[\"bdoc\"]},\"application/x-bittorrent\":{\"source\":\"apache\",\"extensions\":[\"torrent\"]},\"application/x-blorb\":{\"source\":\"apache\",\"extensions\":[\"blb\",\"blorb\"]},\"application/x-bzip\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"bz\"]},\"application/x-bzip2\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"bz2\",\"boz\"]},\"application/x-cbr\":{\"source\":\"apache\",\"extensions\":[\"cbr\",\"cba\",\"cbt\",\"cbz\",\"cb7\"]},\"application/x-cdlink\":{\"source\":\"apache\",\"extensions\":[\"vcd\"]},\"application/x-cfs-compressed\":{\"source\":\"apache\",\"extensions\":[\"cfs\"]},\"application/x-chat\":{\"source\":\"apache\",\"extensions\":[\"chat\"]},\"application/x-chess-pgn\":{\"source\":\"apache\",\"extensions\":[\"pgn\"]},\"application/x-chrome-extension\":{\"extensions\":[\"crx\"]},\"application/x-cocoa\":{\"source\":\"nginx\",\"extensions\":[\"cco\"]},\"application/x-compress\":{\"source\":\"apache\"},\"application/x-conference\":{\"source\":\"apache\",\"extensions\":[\"nsc\"]},\"application/x-cpio\":{\"source\":\"apache\",\"extensions\":[\"cpio\"]},\"application/x-csh\":{\"source\":\"apache\",\"extensions\":[\"csh\"]},\"application/x-deb\":{\"compressible\":false},\"application/x-debian-package\":{\"source\":\"apache\",\"extensions\":[\"deb\",\"udeb\"]},\"application/x-dgc-compressed\":{\"source\":\"apache\",\"extensions\":[\"dgc\"]},\"application/x-director\":{\"source\":\"apache\",\"extensions\":[\"dir\",\"dcr\",\"dxr\",\"cst\",\"cct\",\"cxt\",\"w3d\",\"fgd\",\"swa\"]},\"application/x-doom\":{\"source\":\"apache\",\"extensions\":[\"wad\"]},\"application/x-dtbncx+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"ncx\"]},\"application/x-dtbook+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"dtb\"]},\"application/x-dtbresource+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"res\"]},\"application/x-dvi\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"dvi\"]},\"application/x-envoy\":{\"source\":\"apache\",\"extensions\":[\"evy\"]},\"application/x-eva\":{\"source\":\"apache\",\"extensions\":[\"eva\"]},\"application/x-font-bdf\":{\"source\":\"apache\",\"extensions\":[\"bdf\"]},\"application/x-font-dos\":{\"source\":\"apache\"},\"application/x-font-framemaker\":{\"source\":\"apache\"},\"application/x-font-ghostscript\":{\"source\":\"apache\",\"extensions\":[\"gsf\"]},\"application/x-font-libgrx\":{\"source\":\"apache\"},\"application/x-font-linux-psf\":{\"source\":\"apache\",\"extensions\":[\"psf\"]},\"application/x-font-pcf\":{\"source\":\"apache\",\"extensions\":[\"pcf\"]},\"application/x-font-snf\":{\"source\":\"apache\",\"extensions\":[\"snf\"]},\"application/x-font-speedo\":{\"source\":\"apache\"},\"application/x-font-sunos-news\":{\"source\":\"apache\"},\"application/x-font-type1\":{\"source\":\"apache\",\"extensions\":[\"pfa\",\"pfb\",\"pfm\",\"afm\"]},\"application/x-font-vfont\":{\"source\":\"apache\"},\"application/x-freearc\":{\"source\":\"apache\",\"extensions\":[\"arc\"]},\"application/x-futuresplash\":{\"source\":\"apache\",\"extensions\":[\"spl\"]},\"application/x-gca-compressed\":{\"source\":\"apache\",\"extensions\":[\"gca\"]},\"application/x-glulx\":{\"source\":\"apache\",\"extensions\":[\"ulx\"]},\"application/x-gnumeric\":{\"source\":\"apache\",\"extensions\":[\"gnumeric\"]},\"application/x-gramps-xml\":{\"source\":\"apache\",\"extensions\":[\"gramps\"]},\"application/x-gtar\":{\"source\":\"apache\",\"extensions\":[\"gtar\"]},\"application/x-gzip\":{\"source\":\"apache\"},\"application/x-hdf\":{\"source\":\"apache\",\"extensions\":[\"hdf\"]},\"application/x-httpd-php\":{\"compressible\":true,\"extensions\":[\"php\"]},\"application/x-install-instructions\":{\"source\":\"apache\",\"extensions\":[\"install\"]},\"application/x-iso9660-image\":{\"source\":\"apache\",\"extensions\":[\"iso\"]},\"application/x-java-archive-diff\":{\"source\":\"nginx\",\"extensions\":[\"jardiff\"]},\"application/x-java-jnlp-file\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"jnlp\"]},\"application/x-javascript\":{\"compressible\":true},\"application/x-keepass2\":{\"extensions\":[\"kdbx\"]},\"application/x-latex\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"latex\"]},\"application/x-lua-bytecode\":{\"extensions\":[\"luac\"]},\"application/x-lzh-compressed\":{\"source\":\"apache\",\"extensions\":[\"lzh\",\"lha\"]},\"application/x-makeself\":{\"source\":\"nginx\",\"extensions\":[\"run\"]},\"application/x-mie\":{\"source\":\"apache\",\"extensions\":[\"mie\"]},\"application/x-mobipocket-ebook\":{\"source\":\"apache\",\"extensions\":[\"prc\",\"mobi\"]},\"application/x-mpegurl\":{\"compressible\":false},\"application/x-ms-application\":{\"source\":\"apache\",\"extensions\":[\"application\"]},\"application/x-ms-shortcut\":{\"source\":\"apache\",\"extensions\":[\"lnk\"]},\"application/x-ms-wmd\":{\"source\":\"apache\",\"extensions\":[\"wmd\"]},\"application/x-ms-wmz\":{\"source\":\"apache\",\"extensions\":[\"wmz\"]},\"application/x-ms-xbap\":{\"source\":\"apache\",\"extensions\":[\"xbap\"]},\"application/x-msaccess\":{\"source\":\"apache\",\"extensions\":[\"mdb\"]},\"application/x-msbinder\":{\"source\":\"apache\",\"extensions\":[\"obd\"]},\"application/x-mscardfile\":{\"source\":\"apache\",\"extensions\":[\"crd\"]},\"application/x-msclip\":{\"source\":\"apache\",\"extensions\":[\"clp\"]},\"application/x-msdos-program\":{\"extensions\":[\"exe\"]},\"application/x-msdownload\":{\"source\":\"apache\",\"extensions\":[\"exe\",\"dll\",\"com\",\"bat\",\"msi\"]},\"application/x-msmediaview\":{\"source\":\"apache\",\"extensions\":[\"mvb\",\"m13\",\"m14\"]},\"application/x-msmetafile\":{\"source\":\"apache\",\"extensions\":[\"wmf\",\"wmz\",\"emf\",\"emz\"]},\"application/x-msmoney\":{\"source\":\"apache\",\"extensions\":[\"mny\"]},\"application/x-mspublisher\":{\"source\":\"apache\",\"extensions\":[\"pub\"]},\"application/x-msschedule\":{\"source\":\"apache\",\"extensions\":[\"scd\"]},\"application/x-msterminal\":{\"source\":\"apache\",\"extensions\":[\"trm\"]},\"application/x-mswrite\":{\"source\":\"apache\",\"extensions\":[\"wri\"]},\"application/x-netcdf\":{\"source\":\"apache\",\"extensions\":[\"nc\",\"cdf\"]},\"application/x-ns-proxy-autoconfig\":{\"compressible\":true,\"extensions\":[\"pac\"]},\"application/x-nzb\":{\"source\":\"apache\",\"extensions\":[\"nzb\"]},\"application/x-perl\":{\"source\":\"nginx\",\"extensions\":[\"pl\",\"pm\"]},\"application/x-pilot\":{\"source\":\"nginx\",\"extensions\":[\"prc\",\"pdb\"]},\"application/x-pkcs12\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"p12\",\"pfx\"]},\"application/x-pkcs7-certificates\":{\"source\":\"apache\",\"extensions\":[\"p7b\",\"spc\"]},\"application/x-pkcs7-certreqresp\":{\"source\":\"apache\",\"extensions\":[\"p7r\"]},\"application/x-pki-message\":{\"source\":\"iana\"},\"application/x-rar-compressed\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"rar\"]},\"application/x-redhat-package-manager\":{\"source\":\"nginx\",\"extensions\":[\"rpm\"]},\"application/x-research-info-systems\":{\"source\":\"apache\",\"extensions\":[\"ris\"]},\"application/x-sea\":{\"source\":\"nginx\",\"extensions\":[\"sea\"]},\"application/x-sh\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"sh\"]},\"application/x-shar\":{\"source\":\"apache\",\"extensions\":[\"shar\"]},\"application/x-shockwave-flash\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"swf\"]},\"application/x-silverlight-app\":{\"source\":\"apache\",\"extensions\":[\"xap\"]},\"application/x-sql\":{\"source\":\"apache\",\"extensions\":[\"sql\"]},\"application/x-stuffit\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"sit\"]},\"application/x-stuffitx\":{\"source\":\"apache\",\"extensions\":[\"sitx\"]},\"application/x-subrip\":{\"source\":\"apache\",\"extensions\":[\"srt\"]},\"application/x-sv4cpio\":{\"source\":\"apache\",\"extensions\":[\"sv4cpio\"]},\"application/x-sv4crc\":{\"source\":\"apache\",\"extensions\":[\"sv4crc\"]},\"application/x-t3vm-image\":{\"source\":\"apache\",\"extensions\":[\"t3\"]},\"application/x-tads\":{\"source\":\"apache\",\"extensions\":[\"gam\"]},\"application/x-tar\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"tar\"]},\"application/x-tcl\":{\"source\":\"apache\",\"extensions\":[\"tcl\",\"tk\"]},\"application/x-tex\":{\"source\":\"apache\",\"extensions\":[\"tex\"]},\"application/x-tex-tfm\":{\"source\":\"apache\",\"extensions\":[\"tfm\"]},\"application/x-texinfo\":{\"source\":\"apache\",\"extensions\":[\"texinfo\",\"texi\"]},\"application/x-tgif\":{\"source\":\"apache\",\"extensions\":[\"obj\"]},\"application/x-ustar\":{\"source\":\"apache\",\"extensions\":[\"ustar\"]},\"application/x-virtualbox-hdd\":{\"compressible\":true,\"extensions\":[\"hdd\"]},\"application/x-virtualbox-ova\":{\"compressible\":true,\"extensions\":[\"ova\"]},\"application/x-virtualbox-ovf\":{\"compressible\":true,\"extensions\":[\"ovf\"]},\"application/x-virtualbox-vbox\":{\"compressible\":true,\"extensions\":[\"vbox\"]},\"application/x-virtualbox-vbox-extpack\":{\"compressible\":false,\"extensions\":[\"vbox-extpack\"]},\"application/x-virtualbox-vdi\":{\"compressible\":true,\"extensions\":[\"vdi\"]},\"application/x-virtualbox-vhd\":{\"compressible\":true,\"extensions\":[\"vhd\"]},\"application/x-virtualbox-vmdk\":{\"compressible\":true,\"extensions\":[\"vmdk\"]},\"application/x-wais-source\":{\"source\":\"apache\",\"extensions\":[\"src\"]},\"application/x-web-app-manifest+json\":{\"compressible\":true,\"extensions\":[\"webapp\"]},\"application/x-www-form-urlencoded\":{\"source\":\"iana\",\"compressible\":true},\"application/x-x509-ca-cert\":{\"source\":\"iana\",\"extensions\":[\"der\",\"crt\",\"pem\"]},\"application/x-x509-ca-ra-cert\":{\"source\":\"iana\"},\"application/x-x509-next-ca-cert\":{\"source\":\"iana\"},\"application/x-xfig\":{\"source\":\"apache\",\"extensions\":[\"fig\"]},\"application/x-xliff+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"xlf\"]},\"application/x-xpinstall\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"xpi\"]},\"application/x-xz\":{\"source\":\"apache\",\"extensions\":[\"xz\"]},\"application/x-zmachine\":{\"source\":\"apache\",\"extensions\":[\"z1\",\"z2\",\"z3\",\"z4\",\"z5\",\"z6\",\"z7\",\"z8\"]},\"application/x400-bp\":{\"source\":\"iana\"},\"application/xacml+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/xaml+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"xaml\"]},\"application/xcap-att+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xav\"]},\"application/xcap-caps+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xca\"]},\"application/xcap-diff+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xdf\"]},\"application/xcap-el+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xel\"]},\"application/xcap-error+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xer\"]},\"application/xcap-ns+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xns\"]},\"application/xcon-conference-info+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/xcon-conference-info-diff+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/xenc+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xenc\"]},\"application/xhtml+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xhtml\",\"xht\"]},\"application/xhtml-voice+xml\":{\"source\":\"apache\",\"compressible\":true},\"application/xliff+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xlf\"]},\"application/xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xml\",\"xsl\",\"xsd\",\"rng\"]},\"application/xml-dtd\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"dtd\"]},\"application/xml-external-parsed-entity\":{\"source\":\"iana\"},\"application/xml-patch+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/xmpp+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/xop+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xop\"]},\"application/xproc+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"xpl\"]},\"application/xslt+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xslt\"]},\"application/xspf+xml\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"xspf\"]},\"application/xv+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"mxml\",\"xhvml\",\"xvml\",\"xvm\"]},\"application/yang\":{\"source\":\"iana\",\"extensions\":[\"yang\"]},\"application/yang-data+json\":{\"source\":\"iana\",\"compressible\":true},\"application/yang-data+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/yang-patch+json\":{\"source\":\"iana\",\"compressible\":true},\"application/yang-patch+xml\":{\"source\":\"iana\",\"compressible\":true},\"application/yin+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"yin\"]},\"application/zip\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"zip\"]},\"application/zlib\":{\"source\":\"iana\"},\"application/zstd\":{\"source\":\"iana\"},\"audio/1d-interleaved-parityfec\":{\"source\":\"iana\"},\"audio/32kadpcm\":{\"source\":\"iana\"},\"audio/3gpp\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"3gpp\"]},\"audio/3gpp2\":{\"source\":\"iana\"},\"audio/aac\":{\"source\":\"iana\"},\"audio/ac3\":{\"source\":\"iana\"},\"audio/adpcm\":{\"source\":\"apache\",\"extensions\":[\"adp\"]},\"audio/amr\":{\"source\":\"iana\"},\"audio/amr-wb\":{\"source\":\"iana\"},\"audio/amr-wb+\":{\"source\":\"iana\"},\"audio/aptx\":{\"source\":\"iana\"},\"audio/asc\":{\"source\":\"iana\"},\"audio/atrac-advanced-lossless\":{\"source\":\"iana\"},\"audio/atrac-x\":{\"source\":\"iana\"},\"audio/atrac3\":{\"source\":\"iana\"},\"audio/basic\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"au\",\"snd\"]},\"audio/bv16\":{\"source\":\"iana\"},\"audio/bv32\":{\"source\":\"iana\"},\"audio/clearmode\":{\"source\":\"iana\"},\"audio/cn\":{\"source\":\"iana\"},\"audio/dat12\":{\"source\":\"iana\"},\"audio/dls\":{\"source\":\"iana\"},\"audio/dsr-es201108\":{\"source\":\"iana\"},\"audio/dsr-es202050\":{\"source\":\"iana\"},\"audio/dsr-es202211\":{\"source\":\"iana\"},\"audio/dsr-es202212\":{\"source\":\"iana\"},\"audio/dv\":{\"source\":\"iana\"},\"audio/dvi4\":{\"source\":\"iana\"},\"audio/eac3\":{\"source\":\"iana\"},\"audio/encaprtp\":{\"source\":\"iana\"},\"audio/evrc\":{\"source\":\"iana\"},\"audio/evrc-qcp\":{\"source\":\"iana\"},\"audio/evrc0\":{\"source\":\"iana\"},\"audio/evrc1\":{\"source\":\"iana\"},\"audio/evrcb\":{\"source\":\"iana\"},\"audio/evrcb0\":{\"source\":\"iana\"},\"audio/evrcb1\":{\"source\":\"iana\"},\"audio/evrcnw\":{\"source\":\"iana\"},\"audio/evrcnw0\":{\"source\":\"iana\"},\"audio/evrcnw1\":{\"source\":\"iana\"},\"audio/evrcwb\":{\"source\":\"iana\"},\"audio/evrcwb0\":{\"source\":\"iana\"},\"audio/evrcwb1\":{\"source\":\"iana\"},\"audio/evs\":{\"source\":\"iana\"},\"audio/flexfec\":{\"source\":\"iana\"},\"audio/fwdred\":{\"source\":\"iana\"},\"audio/g711-0\":{\"source\":\"iana\"},\"audio/g719\":{\"source\":\"iana\"},\"audio/g722\":{\"source\":\"iana\"},\"audio/g7221\":{\"source\":\"iana\"},\"audio/g723\":{\"source\":\"iana\"},\"audio/g726-16\":{\"source\":\"iana\"},\"audio/g726-24\":{\"source\":\"iana\"},\"audio/g726-32\":{\"source\":\"iana\"},\"audio/g726-40\":{\"source\":\"iana\"},\"audio/g728\":{\"source\":\"iana\"},\"audio/g729\":{\"source\":\"iana\"},\"audio/g7291\":{\"source\":\"iana\"},\"audio/g729d\":{\"source\":\"iana\"},\"audio/g729e\":{\"source\":\"iana\"},\"audio/gsm\":{\"source\":\"iana\"},\"audio/gsm-efr\":{\"source\":\"iana\"},\"audio/gsm-hr-08\":{\"source\":\"iana\"},\"audio/ilbc\":{\"source\":\"iana\"},\"audio/ip-mr_v2.5\":{\"source\":\"iana\"},\"audio/isac\":{\"source\":\"apache\"},\"audio/l16\":{\"source\":\"iana\"},\"audio/l20\":{\"source\":\"iana\"},\"audio/l24\":{\"source\":\"iana\",\"compressible\":false},\"audio/l8\":{\"source\":\"iana\"},\"audio/lpc\":{\"source\":\"iana\"},\"audio/melp\":{\"source\":\"iana\"},\"audio/melp1200\":{\"source\":\"iana\"},\"audio/melp2400\":{\"source\":\"iana\"},\"audio/melp600\":{\"source\":\"iana\"},\"audio/mhas\":{\"source\":\"iana\"},\"audio/midi\":{\"source\":\"apache\",\"extensions\":[\"mid\",\"midi\",\"kar\",\"rmi\"]},\"audio/mobile-xmf\":{\"source\":\"iana\",\"extensions\":[\"mxmf\"]},\"audio/mp3\":{\"compressible\":false,\"extensions\":[\"mp3\"]},\"audio/mp4\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"m4a\",\"mp4a\"]},\"audio/mp4a-latm\":{\"source\":\"iana\"},\"audio/mpa\":{\"source\":\"iana\"},\"audio/mpa-robust\":{\"source\":\"iana\"},\"audio/mpeg\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"mpga\",\"mp2\",\"mp2a\",\"mp3\",\"m2a\",\"m3a\"]},\"audio/mpeg4-generic\":{\"source\":\"iana\"},\"audio/musepack\":{\"source\":\"apache\"},\"audio/ogg\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"oga\",\"ogg\",\"spx\"]},\"audio/opus\":{\"source\":\"iana\"},\"audio/parityfec\":{\"source\":\"iana\"},\"audio/pcma\":{\"source\":\"iana\"},\"audio/pcma-wb\":{\"source\":\"iana\"},\"audio/pcmu\":{\"source\":\"iana\"},\"audio/pcmu-wb\":{\"source\":\"iana\"},\"audio/prs.sid\":{\"source\":\"iana\"},\"audio/qcelp\":{\"source\":\"iana\"},\"audio/raptorfec\":{\"source\":\"iana\"},\"audio/red\":{\"source\":\"iana\"},\"audio/rtp-enc-aescm128\":{\"source\":\"iana\"},\"audio/rtp-midi\":{\"source\":\"iana\"},\"audio/rtploopback\":{\"source\":\"iana\"},\"audio/rtx\":{\"source\":\"iana\"},\"audio/s3m\":{\"source\":\"apache\",\"extensions\":[\"s3m\"]},\"audio/silk\":{\"source\":\"apache\",\"extensions\":[\"sil\"]},\"audio/smv\":{\"source\":\"iana\"},\"audio/smv-qcp\":{\"source\":\"iana\"},\"audio/smv0\":{\"source\":\"iana\"},\"audio/sp-midi\":{\"source\":\"iana\"},\"audio/speex\":{\"source\":\"iana\"},\"audio/t140c\":{\"source\":\"iana\"},\"audio/t38\":{\"source\":\"iana\"},\"audio/telephone-event\":{\"source\":\"iana\"},\"audio/tetra_acelp\":{\"source\":\"iana\"},\"audio/tetra_acelp_bb\":{\"source\":\"iana\"},\"audio/tone\":{\"source\":\"iana\"},\"audio/uemclip\":{\"source\":\"iana\"},\"audio/ulpfec\":{\"source\":\"iana\"},\"audio/usac\":{\"source\":\"iana\"},\"audio/vdvi\":{\"source\":\"iana\"},\"audio/vmr-wb\":{\"source\":\"iana\"},\"audio/vnd.3gpp.iufp\":{\"source\":\"iana\"},\"audio/vnd.4sb\":{\"source\":\"iana\"},\"audio/vnd.audiokoz\":{\"source\":\"iana\"},\"audio/vnd.celp\":{\"source\":\"iana\"},\"audio/vnd.cisco.nse\":{\"source\":\"iana\"},\"audio/vnd.cmles.radio-events\":{\"source\":\"iana\"},\"audio/vnd.cns.anp1\":{\"source\":\"iana\"},\"audio/vnd.cns.inf1\":{\"source\":\"iana\"},\"audio/vnd.dece.audio\":{\"source\":\"iana\",\"extensions\":[\"uva\",\"uvva\"]},\"audio/vnd.digital-winds\":{\"source\":\"iana\",\"extensions\":[\"eol\"]},\"audio/vnd.dlna.adts\":{\"source\":\"iana\"},\"audio/vnd.dolby.heaac.1\":{\"source\":\"iana\"},\"audio/vnd.dolby.heaac.2\":{\"source\":\"iana\"},\"audio/vnd.dolby.mlp\":{\"source\":\"iana\"},\"audio/vnd.dolby.mps\":{\"source\":\"iana\"},\"audio/vnd.dolby.pl2\":{\"source\":\"iana\"},\"audio/vnd.dolby.pl2x\":{\"source\":\"iana\"},\"audio/vnd.dolby.pl2z\":{\"source\":\"iana\"},\"audio/vnd.dolby.pulse.1\":{\"source\":\"iana\"},\"audio/vnd.dra\":{\"source\":\"iana\",\"extensions\":[\"dra\"]},\"audio/vnd.dts\":{\"source\":\"iana\",\"extensions\":[\"dts\"]},\"audio/vnd.dts.hd\":{\"source\":\"iana\",\"extensions\":[\"dtshd\"]},\"audio/vnd.dts.uhd\":{\"source\":\"iana\"},\"audio/vnd.dvb.file\":{\"source\":\"iana\"},\"audio/vnd.everad.plj\":{\"source\":\"iana\"},\"audio/vnd.hns.audio\":{\"source\":\"iana\"},\"audio/vnd.lucent.voice\":{\"source\":\"iana\",\"extensions\":[\"lvp\"]},\"audio/vnd.ms-playready.media.pya\":{\"source\":\"iana\",\"extensions\":[\"pya\"]},\"audio/vnd.nokia.mobile-xmf\":{\"source\":\"iana\"},\"audio/vnd.nortel.vbk\":{\"source\":\"iana\"},\"audio/vnd.nuera.ecelp4800\":{\"source\":\"iana\",\"extensions\":[\"ecelp4800\"]},\"audio/vnd.nuera.ecelp7470\":{\"source\":\"iana\",\"extensions\":[\"ecelp7470\"]},\"audio/vnd.nuera.ecelp9600\":{\"source\":\"iana\",\"extensions\":[\"ecelp9600\"]},\"audio/vnd.octel.sbc\":{\"source\":\"iana\"},\"audio/vnd.presonus.multitrack\":{\"source\":\"iana\"},\"audio/vnd.qcelp\":{\"source\":\"iana\"},\"audio/vnd.rhetorex.32kadpcm\":{\"source\":\"iana\"},\"audio/vnd.rip\":{\"source\":\"iana\",\"extensions\":[\"rip\"]},\"audio/vnd.rn-realaudio\":{\"compressible\":false},\"audio/vnd.sealedmedia.softseal.mpeg\":{\"source\":\"iana\"},\"audio/vnd.vmx.cvsd\":{\"source\":\"iana\"},\"audio/vnd.wave\":{\"compressible\":false},\"audio/vorbis\":{\"source\":\"iana\",\"compressible\":false},\"audio/vorbis-config\":{\"source\":\"iana\"},\"audio/wav\":{\"compressible\":false,\"extensions\":[\"wav\"]},\"audio/wave\":{\"compressible\":false,\"extensions\":[\"wav\"]},\"audio/webm\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"weba\"]},\"audio/x-aac\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"aac\"]},\"audio/x-aiff\":{\"source\":\"apache\",\"extensions\":[\"aif\",\"aiff\",\"aifc\"]},\"audio/x-caf\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"caf\"]},\"audio/x-flac\":{\"source\":\"apache\",\"extensions\":[\"flac\"]},\"audio/x-m4a\":{\"source\":\"nginx\",\"extensions\":[\"m4a\"]},\"audio/x-matroska\":{\"source\":\"apache\",\"extensions\":[\"mka\"]},\"audio/x-mpegurl\":{\"source\":\"apache\",\"extensions\":[\"m3u\"]},\"audio/x-ms-wax\":{\"source\":\"apache\",\"extensions\":[\"wax\"]},\"audio/x-ms-wma\":{\"source\":\"apache\",\"extensions\":[\"wma\"]},\"audio/x-pn-realaudio\":{\"source\":\"apache\",\"extensions\":[\"ram\",\"ra\"]},\"audio/x-pn-realaudio-plugin\":{\"source\":\"apache\",\"extensions\":[\"rmp\"]},\"audio/x-realaudio\":{\"source\":\"nginx\",\"extensions\":[\"ra\"]},\"audio/x-tta\":{\"source\":\"apache\"},\"audio/x-wav\":{\"source\":\"apache\",\"extensions\":[\"wav\"]},\"audio/xm\":{\"source\":\"apache\",\"extensions\":[\"xm\"]},\"chemical/x-cdx\":{\"source\":\"apache\",\"extensions\":[\"cdx\"]},\"chemical/x-cif\":{\"source\":\"apache\",\"extensions\":[\"cif\"]},\"chemical/x-cmdf\":{\"source\":\"apache\",\"extensions\":[\"cmdf\"]},\"chemical/x-cml\":{\"source\":\"apache\",\"extensions\":[\"cml\"]},\"chemical/x-csml\":{\"source\":\"apache\",\"extensions\":[\"csml\"]},\"chemical/x-pdb\":{\"source\":\"apache\"},\"chemical/x-xyz\":{\"source\":\"apache\",\"extensions\":[\"xyz\"]},\"font/collection\":{\"source\":\"iana\",\"extensions\":[\"ttc\"]},\"font/otf\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"otf\"]},\"font/sfnt\":{\"source\":\"iana\"},\"font/ttf\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"ttf\"]},\"font/woff\":{\"source\":\"iana\",\"extensions\":[\"woff\"]},\"font/woff2\":{\"source\":\"iana\",\"extensions\":[\"woff2\"]},\"image/aces\":{\"source\":\"iana\",\"extensions\":[\"exr\"]},\"image/apng\":{\"compressible\":false,\"extensions\":[\"apng\"]},\"image/avci\":{\"source\":\"iana\"},\"image/avcs\":{\"source\":\"iana\"},\"image/bmp\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"bmp\"]},\"image/cgm\":{\"source\":\"iana\",\"extensions\":[\"cgm\"]},\"image/dicom-rle\":{\"source\":\"iana\",\"extensions\":[\"drle\"]},\"image/emf\":{\"source\":\"iana\",\"extensions\":[\"emf\"]},\"image/fits\":{\"source\":\"iana\",\"extensions\":[\"fits\"]},\"image/g3fax\":{\"source\":\"iana\",\"extensions\":[\"g3\"]},\"image/gif\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"gif\"]},\"image/heic\":{\"source\":\"iana\",\"extensions\":[\"heic\"]},\"image/heic-sequence\":{\"source\":\"iana\",\"extensions\":[\"heics\"]},\"image/heif\":{\"source\":\"iana\",\"extensions\":[\"heif\"]},\"image/heif-sequence\":{\"source\":\"iana\",\"extensions\":[\"heifs\"]},\"image/hej2k\":{\"source\":\"iana\",\"extensions\":[\"hej2\"]},\"image/hsj2\":{\"source\":\"iana\",\"extensions\":[\"hsj2\"]},\"image/ief\":{\"source\":\"iana\",\"extensions\":[\"ief\"]},\"image/jls\":{\"source\":\"iana\",\"extensions\":[\"jls\"]},\"image/jp2\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"jp2\",\"jpg2\"]},\"image/jpeg\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"jpeg\",\"jpg\",\"jpe\"]},\"image/jph\":{\"source\":\"iana\",\"extensions\":[\"jph\"]},\"image/jphc\":{\"source\":\"iana\",\"extensions\":[\"jhc\"]},\"image/jpm\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"jpm\"]},\"image/jpx\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"jpx\",\"jpf\"]},\"image/jxr\":{\"source\":\"iana\",\"extensions\":[\"jxr\"]},\"image/jxra\":{\"source\":\"iana\",\"extensions\":[\"jxra\"]},\"image/jxrs\":{\"source\":\"iana\",\"extensions\":[\"jxrs\"]},\"image/jxs\":{\"source\":\"iana\",\"extensions\":[\"jxs\"]},\"image/jxsc\":{\"source\":\"iana\",\"extensions\":[\"jxsc\"]},\"image/jxsi\":{\"source\":\"iana\",\"extensions\":[\"jxsi\"]},\"image/jxss\":{\"source\":\"iana\",\"extensions\":[\"jxss\"]},\"image/ktx\":{\"source\":\"iana\",\"extensions\":[\"ktx\"]},\"image/naplps\":{\"source\":\"iana\"},\"image/pjpeg\":{\"compressible\":false},\"image/png\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"png\"]},\"image/prs.btif\":{\"source\":\"iana\",\"extensions\":[\"btif\"]},\"image/prs.pti\":{\"source\":\"iana\",\"extensions\":[\"pti\"]},\"image/pwg-raster\":{\"source\":\"iana\"},\"image/sgi\":{\"source\":\"apache\",\"extensions\":[\"sgi\"]},\"image/svg+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"svg\",\"svgz\"]},\"image/t38\":{\"source\":\"iana\",\"extensions\":[\"t38\"]},\"image/tiff\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"tif\",\"tiff\"]},\"image/tiff-fx\":{\"source\":\"iana\",\"extensions\":[\"tfx\"]},\"image/vnd.adobe.photoshop\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"psd\"]},\"image/vnd.airzip.accelerator.azv\":{\"source\":\"iana\",\"extensions\":[\"azv\"]},\"image/vnd.cns.inf2\":{\"source\":\"iana\"},\"image/vnd.dece.graphic\":{\"source\":\"iana\",\"extensions\":[\"uvi\",\"uvvi\",\"uvg\",\"uvvg\"]},\"image/vnd.djvu\":{\"source\":\"iana\",\"extensions\":[\"djvu\",\"djv\"]},\"image/vnd.dvb.subtitle\":{\"source\":\"iana\",\"extensions\":[\"sub\"]},\"image/vnd.dwg\":{\"source\":\"iana\",\"extensions\":[\"dwg\"]},\"image/vnd.dxf\":{\"source\":\"iana\",\"extensions\":[\"dxf\"]},\"image/vnd.fastbidsheet\":{\"source\":\"iana\",\"extensions\":[\"fbs\"]},\"image/vnd.fpx\":{\"source\":\"iana\",\"extensions\":[\"fpx\"]},\"image/vnd.fst\":{\"source\":\"iana\",\"extensions\":[\"fst\"]},\"image/vnd.fujixerox.edmics-mmr\":{\"source\":\"iana\",\"extensions\":[\"mmr\"]},\"image/vnd.fujixerox.edmics-rlc\":{\"source\":\"iana\",\"extensions\":[\"rlc\"]},\"image/vnd.globalgraphics.pgb\":{\"source\":\"iana\"},\"image/vnd.microsoft.icon\":{\"source\":\"iana\",\"extensions\":[\"ico\"]},\"image/vnd.mix\":{\"source\":\"iana\"},\"image/vnd.mozilla.apng\":{\"source\":\"iana\"},\"image/vnd.ms-dds\":{\"extensions\":[\"dds\"]},\"image/vnd.ms-modi\":{\"source\":\"iana\",\"extensions\":[\"mdi\"]},\"image/vnd.ms-photo\":{\"source\":\"apache\",\"extensions\":[\"wdp\"]},\"image/vnd.net-fpx\":{\"source\":\"iana\",\"extensions\":[\"npx\"]},\"image/vnd.radiance\":{\"source\":\"iana\"},\"image/vnd.sealed.png\":{\"source\":\"iana\"},\"image/vnd.sealedmedia.softseal.gif\":{\"source\":\"iana\"},\"image/vnd.sealedmedia.softseal.jpg\":{\"source\":\"iana\"},\"image/vnd.svf\":{\"source\":\"iana\"},\"image/vnd.tencent.tap\":{\"source\":\"iana\",\"extensions\":[\"tap\"]},\"image/vnd.valve.source.texture\":{\"source\":\"iana\",\"extensions\":[\"vtf\"]},\"image/vnd.wap.wbmp\":{\"source\":\"iana\",\"extensions\":[\"wbmp\"]},\"image/vnd.xiff\":{\"source\":\"iana\",\"extensions\":[\"xif\"]},\"image/vnd.zbrush.pcx\":{\"source\":\"iana\",\"extensions\":[\"pcx\"]},\"image/webp\":{\"source\":\"apache\",\"extensions\":[\"webp\"]},\"image/wmf\":{\"source\":\"iana\",\"extensions\":[\"wmf\"]},\"image/x-3ds\":{\"source\":\"apache\",\"extensions\":[\"3ds\"]},\"image/x-cmu-raster\":{\"source\":\"apache\",\"extensions\":[\"ras\"]},\"image/x-cmx\":{\"source\":\"apache\",\"extensions\":[\"cmx\"]},\"image/x-freehand\":{\"source\":\"apache\",\"extensions\":[\"fh\",\"fhc\",\"fh4\",\"fh5\",\"fh7\"]},\"image/x-icon\":{\"source\":\"apache\",\"compressible\":true,\"extensions\":[\"ico\"]},\"image/x-jng\":{\"source\":\"nginx\",\"extensions\":[\"jng\"]},\"image/x-mrsid-image\":{\"source\":\"apache\",\"extensions\":[\"sid\"]},\"image/x-ms-bmp\":{\"source\":\"nginx\",\"compressible\":true,\"extensions\":[\"bmp\"]},\"image/x-pcx\":{\"source\":\"apache\",\"extensions\":[\"pcx\"]},\"image/x-pict\":{\"source\":\"apache\",\"extensions\":[\"pic\",\"pct\"]},\"image/x-portable-anymap\":{\"source\":\"apache\",\"extensions\":[\"pnm\"]},\"image/x-portable-bitmap\":{\"source\":\"apache\",\"extensions\":[\"pbm\"]},\"image/x-portable-graymap\":{\"source\":\"apache\",\"extensions\":[\"pgm\"]},\"image/x-portable-pixmap\":{\"source\":\"apache\",\"extensions\":[\"ppm\"]},\"image/x-rgb\":{\"source\":\"apache\",\"extensions\":[\"rgb\"]},\"image/x-tga\":{\"source\":\"apache\",\"extensions\":[\"tga\"]},\"image/x-xbitmap\":{\"source\":\"apache\",\"extensions\":[\"xbm\"]},\"image/x-xcf\":{\"compressible\":false},\"image/x-xpixmap\":{\"source\":\"apache\",\"extensions\":[\"xpm\"]},\"image/x-xwindowdump\":{\"source\":\"apache\",\"extensions\":[\"xwd\"]},\"message/cpim\":{\"source\":\"iana\"},\"message/delivery-status\":{\"source\":\"iana\"},\"message/disposition-notification\":{\"source\":\"iana\",\"extensions\":[\"disposition-notification\"]},\"message/external-body\":{\"source\":\"iana\"},\"message/feedback-report\":{\"source\":\"iana\"},\"message/global\":{\"source\":\"iana\",\"extensions\":[\"u8msg\"]},\"message/global-delivery-status\":{\"source\":\"iana\",\"extensions\":[\"u8dsn\"]},\"message/global-disposition-notification\":{\"source\":\"iana\",\"extensions\":[\"u8mdn\"]},\"message/global-headers\":{\"source\":\"iana\",\"extensions\":[\"u8hdr\"]},\"message/http\":{\"source\":\"iana\",\"compressible\":false},\"message/imdn+xml\":{\"source\":\"iana\",\"compressible\":true},\"message/news\":{\"source\":\"iana\"},\"message/partial\":{\"source\":\"iana\",\"compressible\":false},\"message/rfc822\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"eml\",\"mime\"]},\"message/s-http\":{\"source\":\"iana\"},\"message/sip\":{\"source\":\"iana\"},\"message/sipfrag\":{\"source\":\"iana\"},\"message/tracking-status\":{\"source\":\"iana\"},\"message/vnd.si.simp\":{\"source\":\"iana\"},\"message/vnd.wfa.wsc\":{\"source\":\"iana\",\"extensions\":[\"wsc\"]},\"model/3mf\":{\"source\":\"iana\",\"extensions\":[\"3mf\"]},\"model/gltf+json\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"gltf\"]},\"model/gltf-binary\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"glb\"]},\"model/iges\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"igs\",\"iges\"]},\"model/mesh\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"msh\",\"mesh\",\"silo\"]},\"model/mtl\":{\"source\":\"iana\",\"extensions\":[\"mtl\"]},\"model/obj\":{\"source\":\"iana\",\"extensions\":[\"obj\"]},\"model/stl\":{\"source\":\"iana\",\"extensions\":[\"stl\"]},\"model/vnd.collada+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"dae\"]},\"model/vnd.dwf\":{\"source\":\"iana\",\"extensions\":[\"dwf\"]},\"model/vnd.flatland.3dml\":{\"source\":\"iana\"},\"model/vnd.gdl\":{\"source\":\"iana\",\"extensions\":[\"gdl\"]},\"model/vnd.gs-gdl\":{\"source\":\"apache\"},\"model/vnd.gs.gdl\":{\"source\":\"iana\"},\"model/vnd.gtw\":{\"source\":\"iana\",\"extensions\":[\"gtw\"]},\"model/vnd.moml+xml\":{\"source\":\"iana\",\"compressible\":true},\"model/vnd.mts\":{\"source\":\"iana\",\"extensions\":[\"mts\"]},\"model/vnd.opengex\":{\"source\":\"iana\",\"extensions\":[\"ogex\"]},\"model/vnd.parasolid.transmit.binary\":{\"source\":\"iana\",\"extensions\":[\"x_b\"]},\"model/vnd.parasolid.transmit.text\":{\"source\":\"iana\",\"extensions\":[\"x_t\"]},\"model/vnd.rosette.annotated-data-model\":{\"source\":\"iana\"},\"model/vnd.usdz+zip\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"usdz\"]},\"model/vnd.valve.source.compiled-map\":{\"source\":\"iana\",\"extensions\":[\"bsp\"]},\"model/vnd.vtu\":{\"source\":\"iana\",\"extensions\":[\"vtu\"]},\"model/vrml\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"wrl\",\"vrml\"]},\"model/x3d+binary\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"x3db\",\"x3dbz\"]},\"model/x3d+fastinfoset\":{\"source\":\"iana\",\"extensions\":[\"x3db\"]},\"model/x3d+vrml\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"x3dv\",\"x3dvz\"]},\"model/x3d+xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"x3d\",\"x3dz\"]},\"model/x3d-vrml\":{\"source\":\"iana\",\"extensions\":[\"x3dv\"]},\"multipart/alternative\":{\"source\":\"iana\",\"compressible\":false},\"multipart/appledouble\":{\"source\":\"iana\"},\"multipart/byteranges\":{\"source\":\"iana\"},\"multipart/digest\":{\"source\":\"iana\"},\"multipart/encrypted\":{\"source\":\"iana\",\"compressible\":false},\"multipart/form-data\":{\"source\":\"iana\",\"compressible\":false},\"multipart/header-set\":{\"source\":\"iana\"},\"multipart/mixed\":{\"source\":\"iana\"},\"multipart/multilingual\":{\"source\":\"iana\"},\"multipart/parallel\":{\"source\":\"iana\"},\"multipart/related\":{\"source\":\"iana\",\"compressible\":false},\"multipart/report\":{\"source\":\"iana\"},\"multipart/signed\":{\"source\":\"iana\",\"compressible\":false},\"multipart/vnd.bint.med-plus\":{\"source\":\"iana\"},\"multipart/voice-message\":{\"source\":\"iana\"},\"multipart/x-mixed-replace\":{\"source\":\"iana\"},\"text/1d-interleaved-parityfec\":{\"source\":\"iana\"},\"text/cache-manifest\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"appcache\",\"manifest\"]},\"text/calendar\":{\"source\":\"iana\",\"extensions\":[\"ics\",\"ifb\"]},\"text/calender\":{\"compressible\":true},\"text/cmd\":{\"compressible\":true},\"text/coffeescript\":{\"extensions\":[\"coffee\",\"litcoffee\"]},\"text/css\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"css\"]},\"text/csv\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"csv\"]},\"text/csv-schema\":{\"source\":\"iana\"},\"text/directory\":{\"source\":\"iana\"},\"text/dns\":{\"source\":\"iana\"},\"text/ecmascript\":{\"source\":\"iana\"},\"text/encaprtp\":{\"source\":\"iana\"},\"text/enriched\":{\"source\":\"iana\"},\"text/flexfec\":{\"source\":\"iana\"},\"text/fwdred\":{\"source\":\"iana\"},\"text/grammar-ref-list\":{\"source\":\"iana\"},\"text/html\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"html\",\"htm\",\"shtml\"]},\"text/jade\":{\"extensions\":[\"jade\"]},\"text/javascript\":{\"source\":\"iana\",\"compressible\":true},\"text/jcr-cnd\":{\"source\":\"iana\"},\"text/jsx\":{\"compressible\":true,\"extensions\":[\"jsx\"]},\"text/less\":{\"compressible\":true,\"extensions\":[\"less\"]},\"text/markdown\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"markdown\",\"md\"]},\"text/mathml\":{\"source\":\"nginx\",\"extensions\":[\"mml\"]},\"text/mdx\":{\"compressible\":true,\"extensions\":[\"mdx\"]},\"text/mizar\":{\"source\":\"iana\"},\"text/n3\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"n3\"]},\"text/parameters\":{\"source\":\"iana\",\"charset\":\"UTF-8\"},\"text/parityfec\":{\"source\":\"iana\"},\"text/plain\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"txt\",\"text\",\"conf\",\"def\",\"list\",\"log\",\"in\",\"ini\"]},\"text/provenance-notation\":{\"source\":\"iana\",\"charset\":\"UTF-8\"},\"text/prs.fallenstein.rst\":{\"source\":\"iana\"},\"text/prs.lines.tag\":{\"source\":\"iana\",\"extensions\":[\"dsc\"]},\"text/prs.prop.logic\":{\"source\":\"iana\"},\"text/raptorfec\":{\"source\":\"iana\"},\"text/red\":{\"source\":\"iana\"},\"text/rfc822-headers\":{\"source\":\"iana\"},\"text/richtext\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rtx\"]},\"text/rtf\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"rtf\"]},\"text/rtp-enc-aescm128\":{\"source\":\"iana\"},\"text/rtploopback\":{\"source\":\"iana\"},\"text/rtx\":{\"source\":\"iana\"},\"text/sgml\":{\"source\":\"iana\",\"extensions\":[\"sgml\",\"sgm\"]},\"text/shex\":{\"extensions\":[\"shex\"]},\"text/slim\":{\"extensions\":[\"slim\",\"slm\"]},\"text/strings\":{\"source\":\"iana\"},\"text/stylus\":{\"extensions\":[\"stylus\",\"styl\"]},\"text/t140\":{\"source\":\"iana\"},\"text/tab-separated-values\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"tsv\"]},\"text/troff\":{\"source\":\"iana\",\"extensions\":[\"t\",\"tr\",\"roff\",\"man\",\"me\",\"ms\"]},\"text/turtle\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"extensions\":[\"ttl\"]},\"text/ulpfec\":{\"source\":\"iana\"},\"text/uri-list\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"uri\",\"uris\",\"urls\"]},\"text/vcard\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"vcard\"]},\"text/vnd.a\":{\"source\":\"iana\"},\"text/vnd.abc\":{\"source\":\"iana\"},\"text/vnd.ascii-art\":{\"source\":\"iana\"},\"text/vnd.curl\":{\"source\":\"iana\",\"extensions\":[\"curl\"]},\"text/vnd.curl.dcurl\":{\"source\":\"apache\",\"extensions\":[\"dcurl\"]},\"text/vnd.curl.mcurl\":{\"source\":\"apache\",\"extensions\":[\"mcurl\"]},\"text/vnd.curl.scurl\":{\"source\":\"apache\",\"extensions\":[\"scurl\"]},\"text/vnd.debian.copyright\":{\"source\":\"iana\",\"charset\":\"UTF-8\"},\"text/vnd.dmclientscript\":{\"source\":\"iana\"},\"text/vnd.dvb.subtitle\":{\"source\":\"iana\",\"extensions\":[\"sub\"]},\"text/vnd.esmertec.theme-descriptor\":{\"source\":\"iana\",\"charset\":\"UTF-8\"},\"text/vnd.ficlab.flt\":{\"source\":\"iana\"},\"text/vnd.fly\":{\"source\":\"iana\",\"extensions\":[\"fly\"]},\"text/vnd.fmi.flexstor\":{\"source\":\"iana\",\"extensions\":[\"flx\"]},\"text/vnd.gml\":{\"source\":\"iana\"},\"text/vnd.graphviz\":{\"source\":\"iana\",\"extensions\":[\"gv\"]},\"text/vnd.hgl\":{\"source\":\"iana\"},\"text/vnd.in3d.3dml\":{\"source\":\"iana\",\"extensions\":[\"3dml\"]},\"text/vnd.in3d.spot\":{\"source\":\"iana\",\"extensions\":[\"spot\"]},\"text/vnd.iptc.newsml\":{\"source\":\"iana\"},\"text/vnd.iptc.nitf\":{\"source\":\"iana\"},\"text/vnd.latex-z\":{\"source\":\"iana\"},\"text/vnd.motorola.reflex\":{\"source\":\"iana\"},\"text/vnd.ms-mediapackage\":{\"source\":\"iana\"},\"text/vnd.net2phone.commcenter.command\":{\"source\":\"iana\"},\"text/vnd.radisys.msml-basic-layout\":{\"source\":\"iana\"},\"text/vnd.senx.warpscript\":{\"source\":\"iana\"},\"text/vnd.si.uricatalogue\":{\"source\":\"iana\"},\"text/vnd.sosi\":{\"source\":\"iana\"},\"text/vnd.sun.j2me.app-descriptor\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"extensions\":[\"jad\"]},\"text/vnd.trolltech.linguist\":{\"source\":\"iana\",\"charset\":\"UTF-8\"},\"text/vnd.wap.si\":{\"source\":\"iana\"},\"text/vnd.wap.sl\":{\"source\":\"iana\"},\"text/vnd.wap.wml\":{\"source\":\"iana\",\"extensions\":[\"wml\"]},\"text/vnd.wap.wmlscript\":{\"source\":\"iana\",\"extensions\":[\"wmls\"]},\"text/vtt\":{\"source\":\"iana\",\"charset\":\"UTF-8\",\"compressible\":true,\"extensions\":[\"vtt\"]},\"text/x-asm\":{\"source\":\"apache\",\"extensions\":[\"s\",\"asm\"]},\"text/x-c\":{\"source\":\"apache\",\"extensions\":[\"c\",\"cc\",\"cxx\",\"cpp\",\"h\",\"hh\",\"dic\"]},\"text/x-component\":{\"source\":\"nginx\",\"extensions\":[\"htc\"]},\"text/x-fortran\":{\"source\":\"apache\",\"extensions\":[\"f\",\"for\",\"f77\",\"f90\"]},\"text/x-gwt-rpc\":{\"compressible\":true},\"text/x-handlebars-template\":{\"extensions\":[\"hbs\"]},\"text/x-java-source\":{\"source\":\"apache\",\"extensions\":[\"java\"]},\"text/x-jquery-tmpl\":{\"compressible\":true},\"text/x-lua\":{\"extensions\":[\"lua\"]},\"text/x-markdown\":{\"compressible\":true,\"extensions\":[\"mkd\"]},\"text/x-nfo\":{\"source\":\"apache\",\"extensions\":[\"nfo\"]},\"text/x-opml\":{\"source\":\"apache\",\"extensions\":[\"opml\"]},\"text/x-org\":{\"compressible\":true,\"extensions\":[\"org\"]},\"text/x-pascal\":{\"source\":\"apache\",\"extensions\":[\"p\",\"pas\"]},\"text/x-processing\":{\"compressible\":true,\"extensions\":[\"pde\"]},\"text/x-sass\":{\"extensions\":[\"sass\"]},\"text/x-scss\":{\"extensions\":[\"scss\"]},\"text/x-setext\":{\"source\":\"apache\",\"extensions\":[\"etx\"]},\"text/x-sfv\":{\"source\":\"apache\",\"extensions\":[\"sfv\"]},\"text/x-suse-ymp\":{\"compressible\":true,\"extensions\":[\"ymp\"]},\"text/x-uuencode\":{\"source\":\"apache\",\"extensions\":[\"uu\"]},\"text/x-vcalendar\":{\"source\":\"apache\",\"extensions\":[\"vcs\"]},\"text/x-vcard\":{\"source\":\"apache\",\"extensions\":[\"vcf\"]},\"text/xml\":{\"source\":\"iana\",\"compressible\":true,\"extensions\":[\"xml\"]},\"text/xml-external-parsed-entity\":{\"source\":\"iana\"},\"text/yaml\":{\"extensions\":[\"yaml\",\"yml\"]},\"video/1d-interleaved-parityfec\":{\"source\":\"iana\"},\"video/3gpp\":{\"source\":\"iana\",\"extensions\":[\"3gp\",\"3gpp\"]},\"video/3gpp-tt\":{\"source\":\"iana\"},\"video/3gpp2\":{\"source\":\"iana\",\"extensions\":[\"3g2\"]},\"video/bmpeg\":{\"source\":\"iana\"},\"video/bt656\":{\"source\":\"iana\"},\"video/celb\":{\"source\":\"iana\"},\"video/dv\":{\"source\":\"iana\"},\"video/encaprtp\":{\"source\":\"iana\"},\"video/flexfec\":{\"source\":\"iana\"},\"video/h261\":{\"source\":\"iana\",\"extensions\":[\"h261\"]},\"video/h263\":{\"source\":\"iana\",\"extensions\":[\"h263\"]},\"video/h263-1998\":{\"source\":\"iana\"},\"video/h263-2000\":{\"source\":\"iana\"},\"video/h264\":{\"source\":\"iana\",\"extensions\":[\"h264\"]},\"video/h264-rcdo\":{\"source\":\"iana\"},\"video/h264-svc\":{\"source\":\"iana\"},\"video/h265\":{\"source\":\"iana\"},\"video/iso.segment\":{\"source\":\"iana\"},\"video/jpeg\":{\"source\":\"iana\",\"extensions\":[\"jpgv\"]},\"video/jpeg2000\":{\"source\":\"iana\"},\"video/jpm\":{\"source\":\"apache\",\"extensions\":[\"jpm\",\"jpgm\"]},\"video/mj2\":{\"source\":\"iana\",\"extensions\":[\"mj2\",\"mjp2\"]},\"video/mp1s\":{\"source\":\"iana\"},\"video/mp2p\":{\"source\":\"iana\"},\"video/mp2t\":{\"source\":\"iana\",\"extensions\":[\"ts\"]},\"video/mp4\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"mp4\",\"mp4v\",\"mpg4\"]},\"video/mp4v-es\":{\"source\":\"iana\"},\"video/mpeg\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"mpeg\",\"mpg\",\"mpe\",\"m1v\",\"m2v\"]},\"video/mpeg4-generic\":{\"source\":\"iana\"},\"video/mpv\":{\"source\":\"iana\"},\"video/nv\":{\"source\":\"iana\"},\"video/ogg\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"ogv\"]},\"video/parityfec\":{\"source\":\"iana\"},\"video/pointer\":{\"source\":\"iana\"},\"video/quicktime\":{\"source\":\"iana\",\"compressible\":false,\"extensions\":[\"qt\",\"mov\"]},\"video/raptorfec\":{\"source\":\"iana\"},\"video/raw\":{\"source\":\"iana\"},\"video/rtp-enc-aescm128\":{\"source\":\"iana\"},\"video/rtploopback\":{\"source\":\"iana\"},\"video/rtx\":{\"source\":\"iana\"},\"video/smpte291\":{\"source\":\"iana\"},\"video/smpte292m\":{\"source\":\"iana\"},\"video/ulpfec\":{\"source\":\"iana\"},\"video/vc1\":{\"source\":\"iana\"},\"video/vc2\":{\"source\":\"iana\"},\"video/vnd.cctv\":{\"source\":\"iana\"},\"video/vnd.dece.hd\":{\"source\":\"iana\",\"extensions\":[\"uvh\",\"uvvh\"]},\"video/vnd.dece.mobile\":{\"source\":\"iana\",\"extensions\":[\"uvm\",\"uvvm\"]},\"video/vnd.dece.mp4\":{\"source\":\"iana\"},\"video/vnd.dece.pd\":{\"source\":\"iana\",\"extensions\":[\"uvp\",\"uvvp\"]},\"video/vnd.dece.sd\":{\"source\":\"iana\",\"extensions\":[\"uvs\",\"uvvs\"]},\"video/vnd.dece.video\":{\"source\":\"iana\",\"extensions\":[\"uvv\",\"uvvv\"]},\"video/vnd.directv.mpeg\":{\"source\":\"iana\"},\"video/vnd.directv.mpeg-tts\":{\"source\":\"iana\"},\"video/vnd.dlna.mpeg-tts\":{\"source\":\"iana\"},\"video/vnd.dvb.file\":{\"source\":\"iana\",\"extensions\":[\"dvb\"]},\"video/vnd.fvt\":{\"source\":\"iana\",\"extensions\":[\"fvt\"]},\"video/vnd.hns.video\":{\"source\":\"iana\"},\"video/vnd.iptvforum.1dparityfec-1010\":{\"source\":\"iana\"},\"video/vnd.iptvforum.1dparityfec-2005\":{\"source\":\"iana\"},\"video/vnd.iptvforum.2dparityfec-1010\":{\"source\":\"iana\"},\"video/vnd.iptvforum.2dparityfec-2005\":{\"source\":\"iana\"},\"video/vnd.iptvforum.ttsavc\":{\"source\":\"iana\"},\"video/vnd.iptvforum.ttsmpeg2\":{\"source\":\"iana\"},\"video/vnd.motorola.video\":{\"source\":\"iana\"},\"video/vnd.motorola.videop\":{\"source\":\"iana\"},\"video/vnd.mpegurl\":{\"source\":\"iana\",\"extensions\":[\"mxu\",\"m4u\"]},\"video/vnd.ms-playready.media.pyv\":{\"source\":\"iana\",\"extensions\":[\"pyv\"]},\"video/vnd.nokia.interleaved-multimedia\":{\"source\":\"iana\"},\"video/vnd.nokia.mp4vr\":{\"source\":\"iana\"},\"video/vnd.nokia.videovoip\":{\"source\":\"iana\"},\"video/vnd.objectvideo\":{\"source\":\"iana\"},\"video/vnd.radgamettools.bink\":{\"source\":\"iana\"},\"video/vnd.radgamettools.smacker\":{\"source\":\"iana\"},\"video/vnd.sealed.mpeg1\":{\"source\":\"iana\"},\"video/vnd.sealed.mpeg4\":{\"source\":\"iana\"},\"video/vnd.sealed.swf\":{\"source\":\"iana\"},\"video/vnd.sealedmedia.softseal.mov\":{\"source\":\"iana\"},\"video/vnd.uvvu.mp4\":{\"source\":\"iana\",\"extensions\":[\"uvu\",\"uvvu\"]},\"video/vnd.vivo\":{\"source\":\"iana\",\"extensions\":[\"viv\"]},\"video/vnd.youtube.yt\":{\"source\":\"iana\"},\"video/vp8\":{\"source\":\"iana\"},\"video/webm\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"webm\"]},\"video/x-f4v\":{\"source\":\"apache\",\"extensions\":[\"f4v\"]},\"video/x-fli\":{\"source\":\"apache\",\"extensions\":[\"fli\"]},\"video/x-flv\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"flv\"]},\"video/x-m4v\":{\"source\":\"apache\",\"extensions\":[\"m4v\"]},\"video/x-matroska\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"mkv\",\"mk3d\",\"mks\"]},\"video/x-mng\":{\"source\":\"apache\",\"extensions\":[\"mng\"]},\"video/x-ms-asf\":{\"source\":\"apache\",\"extensions\":[\"asf\",\"asx\"]},\"video/x-ms-vob\":{\"source\":\"apache\",\"extensions\":[\"vob\"]},\"video/x-ms-wm\":{\"source\":\"apache\",\"extensions\":[\"wm\"]},\"video/x-ms-wmv\":{\"source\":\"apache\",\"compressible\":false,\"extensions\":[\"wmv\"]},\"video/x-ms-wmx\":{\"source\":\"apache\",\"extensions\":[\"wmx\"]},\"video/x-ms-wvx\":{\"source\":\"apache\",\"extensions\":[\"wvx\"]},\"video/x-msvideo\":{\"source\":\"apache\",\"extensions\":[\"avi\"]},\"video/x-sgi-movie\":{\"source\":\"apache\",\"extensions\":[\"movie\"]},\"video/x-smv\":{\"source\":\"apache\",\"extensions\":[\"smv\"]},\"x-conference/x-cooltalk\":{\"source\":\"apache\",\"extensions\":[\"ice\"]},\"x-shader/x-fragment\":{\"compressible\":true},\"x-shader/x-vertex\":{\"compressible\":true}}"); + +/***/ }), + +/***/ "../../node_modules/mime-db/index.js": +/***/ (function(module, exports, __webpack_require__) { + +/*! + * mime-db + * Copyright(c) 2014 Jonathan Ong + * MIT Licensed + */ + +/** + * Module exports. + */ + +module.exports = __webpack_require__("../../node_modules/mime-db/db.json") + + +/***/ }), + +/***/ "../../node_modules/mime-types/index.js": +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * mime-types + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + + + +/** + * Module dependencies. + * @private + */ + +var db = __webpack_require__("../../node_modules/mime-db/index.js") +var extname = __webpack_require__("path").extname + +/** + * Module variables. + * @private + */ + +var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/ +var TEXT_TYPE_REGEXP = /^text\//i + +/** + * Module exports. + * @public + */ + +exports.charset = charset +exports.charsets = { lookup: charset } +exports.contentType = contentType +exports.extension = extension +exports.extensions = Object.create(null) +exports.lookup = lookup +exports.types = Object.create(null) + +// Populate the extensions/types maps +populateMaps(exports.extensions, exports.types) + +/** + * Get the default charset for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + +function charset (type) { + if (!type || typeof type !== 'string') { + return false + } + + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type) + var mime = match && db[match[1].toLowerCase()] + + if (mime && mime.charset) { + return mime.charset + } + + // default text/* to utf-8 + if (match && TEXT_TYPE_REGEXP.test(match[1])) { + return 'UTF-8' + } + + return false +} + +/** + * Create a full Content-Type header given a MIME type or extension. + * + * @param {string} str + * @return {boolean|string} + */ + +function contentType (str) { + // TODO: should this even be in this module? + if (!str || typeof str !== 'string') { + return false + } + + var mime = str.indexOf('/') === -1 + ? exports.lookup(str) + : str + + if (!mime) { + return false + } + + // TODO: use content-type or other module + if (mime.indexOf('charset') === -1) { + var charset = exports.charset(mime) + if (charset) mime += '; charset=' + charset.toLowerCase() + } + + return mime +} + +/** + * Get the default extension for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + +function extension (type) { + if (!type || typeof type !== 'string') { + return false + } + + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type) + + // get extensions + var exts = match && exports.extensions[match[1].toLowerCase()] + + if (!exts || !exts.length) { + return false + } + + return exts[0] +} + +/** + * Lookup the MIME type for a file path/extension. + * + * @param {string} path + * @return {boolean|string} + */ + +function lookup (path) { + if (!path || typeof path !== 'string') { + return false + } + + // get the extension ("ext" or ".ext" or full path) + var extension = extname('x.' + path) + .toLowerCase() + .substr(1) + + if (!extension) { + return false + } + + return exports.types[extension] || false +} + +/** + * Populate the extensions and types maps. + * @private + */ + +function populateMaps (extensions, types) { + // source preference (least -> most) + var preference = ['nginx', 'apache', undefined, 'iana'] + + Object.keys(db).forEach(function forEachMimeType (type) { + var mime = db[type] + var exts = mime.extensions + + if (!exts || !exts.length) { + return + } + + // mime -> extensions + extensions[type] = exts + + // extension -> mime + for (var i = 0; i < exts.length; i++) { + var extension = exts[i] + + if (types[extension]) { + var from = preference.indexOf(db[types[extension]].source) + var to = preference.indexOf(mime.source) + + if (types[extension] !== 'application/octet-stream' && + (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) { + // skip the remapping + continue + } + } + + // set the extension -> mime + types[extension] = type + } + }) +} + + /***/ }), /***/ "../../node_modules/mimic-fn/index.js": @@ -60672,17 +62935,6 @@ async function installBazelTools(repoRootPath) { -async function isVaultAvailable() { - try { - await Object(_child_process__WEBPACK_IMPORTED_MODULE_3__[/* spawn */ "a"])('vault', ['--version'], { - stdio: 'pipe' - }); - return true; - } catch { - return false; - } -} - async function isElasticCommitter() { try { const { @@ -60696,21 +62948,13 @@ async function isElasticCommitter() { } } -async function migrateToNewServersIfNeeded(settingsPath) { +async function upToDate(settingsPath) { if (!(await Object(_fs__WEBPACK_IMPORTED_MODULE_5__[/* isFile */ "d"])(settingsPath))) { return false; } const readSettingsFile = await Object(_fs__WEBPACK_IMPORTED_MODULE_5__[/* readFile */ "f"])(settingsPath, 'utf8'); - const newReadSettingsFile = readSettingsFile.replace(/cloud\.buildbuddy\.io/g, 'remote.buildbuddy.io'); - - if (newReadSettingsFile === readSettingsFile) { - return false; - } - - Object(_fs__WEBPACK_IMPORTED_MODULE_5__[/* writeFile */ "i"])(settingsPath, newReadSettingsFile); - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info(`[bazel_tools] upgrade remote cache settings to use new server address`); - return true; + return readSettingsFile.startsWith('# V2 '); } async function setupRemoteCache(repoRootPath) { @@ -60720,52 +62964,19 @@ async function setupRemoteCache(repoRootPath) { } _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].debug(`[bazel_tools] setting up remote cache settings if necessary`); - const settingsPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(repoRootPath, '.bazelrc.cache'); // Checks if we should upgrade the servers used on .bazelrc.cache - // - // NOTE: this can be removed in the future once everyone is migrated into the new servers - - if (await migrateToNewServersIfNeeded(settingsPath)) { - return; - } - - if (Object(fs__WEBPACK_IMPORTED_MODULE_1__["existsSync"])(settingsPath)) { - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].debug(`[bazel_tools] remote cache settings already exist, skipping`); - return; - } - - if (!(await isVaultAvailable())) { - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info('[bazel_tools] vault is not available, unable to setup remote cache settings.'); - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info('[bazel_tools] building packages will work, but will be slower in many cases.'); - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info('[bazel_tools] use the following guide or reach out to Operations for assistance'); - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info('[bazel_tools] https://github.com/elastic/infra/tree/master/docs/vault'); - return; - } - - let apiKey = ''; + const settingsPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(repoRootPath, '.bazelrc.cache'); // Checks if we should upgrade or install the config file - try { - const { - stdout - } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_3__[/* spawn */ "a"])('vault', ['read', '-field=readonly-key', 'secret/ui-team/kibana-bazel-remote-cache'], { - stdio: 'pipe' - }); - apiKey = stdout.trim(); - } catch (ex) { - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info('[bazel_tools] unable to read bazel remote cache key from vault, are you authenticated?'); - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info('[bazel_tools] building packages will work, but will be slower in many cases.'); - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info('[bazel_tools] reach out to Operations if you need assistance with this.'); - _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info(`[bazel_tools] ${ex}`); + if (await upToDate(settingsPath)) { + _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].debug(`[bazel_tools] remote cache config already exists and is up-to-date, skipping`); return; } const contents = dedent__WEBPACK_IMPORTED_MODULE_0___default.a` - # V1 - This file is automatically generated by 'yarn kbn bootstrap' + # V2 - This file is automatically generated by 'yarn kbn bootstrap' # To regenerate this file, delete it and run 'yarn kbn bootstrap' again. - build --bes_results_url=https://app.buildbuddy.io/invocation/ - build --bes_backend=grpcs://remote.buildbuddy.io - build --remote_cache=grpcs://remote.buildbuddy.io - build --remote_timeout=3600 - build --remote_header=${apiKey} + build --remote_cache=https://storage.googleapis.com/kibana-local-bazel-remote-cache + build --noremote_upload_local_results + build --incompatible_remote_results_ignore_disk `; Object(fs__WEBPACK_IMPORTED_MODULE_1__["writeFileSync"])(settingsPath, contents); _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].info(`[bazel_tools] remote cache settings written to ${settingsPath}`); diff --git a/packages/kbn-pm/src/utils/bazel/setup_remote_cache.ts b/packages/kbn-pm/src/utils/bazel/setup_remote_cache.ts index 0c5213e2dacc6..5c28d6550d0f7 100644 --- a/packages/kbn-pm/src/utils/bazel/setup_remote_cache.ts +++ b/packages/kbn-pm/src/utils/bazel/setup_remote_cache.ts @@ -6,21 +6,11 @@ * Side Public License, v 1. */ import dedent from 'dedent'; -import { existsSync, writeFileSync } from 'fs'; +import { writeFileSync } from 'fs'; import { resolve } from 'path'; import { spawn } from '../child_process'; import { log } from '../log'; -import { isFile, readFile, writeFile } from '../fs'; - -async function isVaultAvailable() { - try { - await spawn('vault', ['--version'], { stdio: 'pipe' }); - - return true; - } catch { - return false; - } -} +import { isFile, readFile } from '../fs'; async function isElasticCommitter() { try { @@ -34,24 +24,13 @@ async function isElasticCommitter() { } } -async function migrateToNewServersIfNeeded(settingsPath: string) { +async function upToDate(settingsPath: string) { if (!(await isFile(settingsPath))) { return false; } const readSettingsFile = await readFile(settingsPath, 'utf8'); - const newReadSettingsFile = readSettingsFile.replace( - /cloud\.buildbuddy\.io/g, - 'remote.buildbuddy.io' - ); - - if (newReadSettingsFile === readSettingsFile) { - return false; - } - - writeFile(settingsPath, newReadSettingsFile); - log.info(`[bazel_tools] upgrade remote cache settings to use new server address`); - return true; + return readSettingsFile.startsWith('# V2 '); } export async function setupRemoteCache(repoRootPath: string) { @@ -64,56 +43,18 @@ export async function setupRemoteCache(repoRootPath: string) { const settingsPath = resolve(repoRootPath, '.bazelrc.cache'); - // Checks if we should upgrade the servers used on .bazelrc.cache - // - // NOTE: this can be removed in the future once everyone is migrated into the new servers - if (await migrateToNewServersIfNeeded(settingsPath)) { - return; - } - - if (existsSync(settingsPath)) { - log.debug(`[bazel_tools] remote cache settings already exist, skipping`); - return; - } - - if (!(await isVaultAvailable())) { - log.info('[bazel_tools] vault is not available, unable to setup remote cache settings.'); - log.info('[bazel_tools] building packages will work, but will be slower in many cases.'); - log.info('[bazel_tools] use the following guide or reach out to Operations for assistance'); - log.info('[bazel_tools] https://github.com/elastic/infra/tree/master/docs/vault'); - return; - } - - let apiKey = ''; - - try { - const { stdout } = await spawn( - 'vault', - ['read', '-field=readonly-key', 'secret/ui-team/kibana-bazel-remote-cache'], - { - stdio: 'pipe', - } - ); - apiKey = stdout.trim(); - } catch (ex: unknown) { - log.info( - '[bazel_tools] unable to read bazel remote cache key from vault, are you authenticated?' - ); - log.info('[bazel_tools] building packages will work, but will be slower in many cases.'); - log.info('[bazel_tools] reach out to Operations if you need assistance with this.'); - log.info(`[bazel_tools] ${ex}`); - + // Checks if we should upgrade or install the config file + if (await upToDate(settingsPath)) { + log.debug(`[bazel_tools] remote cache config already exists and is up-to-date, skipping`); return; } const contents = dedent` - # V1 - This file is automatically generated by 'yarn kbn bootstrap' + # V2 - This file is automatically generated by 'yarn kbn bootstrap' # To regenerate this file, delete it and run 'yarn kbn bootstrap' again. - build --bes_results_url=https://app.buildbuddy.io/invocation/ - build --bes_backend=grpcs://remote.buildbuddy.io - build --remote_cache=grpcs://remote.buildbuddy.io - build --remote_timeout=3600 - build --remote_header=${apiKey} + build --remote_cache=https://storage.googleapis.com/kibana-local-bazel-remote-cache + build --noremote_upload_local_results + build --incompatible_remote_results_ignore_disk `; writeFileSync(settingsPath, contents); diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.tsx b/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.tsx index bffa7c2a5269c..3131b6ab2a73c 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.component.tsx @@ -23,8 +23,8 @@ export interface Props { emptyPromptColor?: EuiEmptyPromptProps['color']; } -const createDataViewText = i18n.translate('sharedUXComponents.noDataViewsPage.addDataViewText', { - defaultMessage: 'Create Data View', +const createDataViewText = i18n.translate('sharedUXComponents.noDataViewsPrompt.addDataViewText', { + defaultMessage: 'Create data view', }); // Using raw value because it is content dependent @@ -50,39 +50,55 @@ export const NoDataViews = ({ ); + const title = canCreateNewDataView ? ( +

+ +
+ +

+ ) : ( +

+ +

+ ); + + const body = canCreateNewDataView ? ( +

+ +

+ ) : ( +

+ +

+ ); + return ( } - title={ -

- -
- -

- } - body={ -

- -

- } + title={title} + body={body} actions={createNewButton} footer={dataViewsDocLink && } /> diff --git a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.tsx b/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.tsx index 19fefd87aa889..8d0e6d93275e1 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/no_data_views/no_data_views.tsx @@ -61,6 +61,7 @@ export const NoDataViews = ({ onDataViewCreated }: Props) => { onSave: (dataView) => { onDataViewCreated(dataView); }, + showEmptyPrompt: false, }); if (setDataViewEditorRef) { 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} +
void; + /** If set to false, will skip empty prompt in data view editor. */ + showEmptyPrompt?: boolean; } /** diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index d2182064d352e..bf652bf8c8444 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -14,6 +14,7 @@ import type { CustomHelpers } from 'joi'; // valid pattern for ID // enforced camel-case identifiers for consistency const ID_PATTERN = /^[a-zA-Z0-9_]+$/; +const SCALABILITY_DURATION_PATTERN = /^[1-9]\d{0,}[m|s]$/; // it will search both --inspect and --inspect-brk const INSPECTING = !!process.execArgv.find((arg) => arg.includes('--inspect')); @@ -264,6 +265,67 @@ export const schema = Joi.object() }) .default(), + /** + * Optional settings to list test data archives, that will be loaded during the 'beforeTests' + * lifecycle phase and unloaded during the 'cleanup' lifecycle phase. + */ + testData: Joi.object() + .keys({ + kbnArchives: Joi.array().items(Joi.string()).default([]), + esArchives: Joi.array().items(Joi.string()).default([]), + }) + .default(), + + /** + * Optional settings to enable scalability testing for single user performance journey. + * If defined, 'scalabilitySetup' must include 'warmup' and 'test' stages, + * 'maxDuration', e.g. '10m' to limit execution time to 10 minutes. + * Each stage must include 'action', 'duration' and 'maxUsersCount'. + * In addition, 'rampConcurrentUsers' requires 'minUsersCount' to ramp users from + * min to max within provided time duration. + */ + scalabilitySetup: Joi.object() + .keys({ + warmup: Joi.object() + .keys({ + stages: Joi.array().items( + Joi.object().keys({ + action: Joi.string() + .valid('constantConcurrentUsers', 'rampConcurrentUsers') + .required(), + duration: Joi.string().pattern(SCALABILITY_DURATION_PATTERN).required(), + minUsersCount: Joi.number().when('action', { + is: 'rampConcurrentUsers', + then: Joi.number().required().less(Joi.ref('maxUsersCount')), + otherwise: Joi.forbidden(), + }), + maxUsersCount: Joi.number().required().greater(0), + }) + ), + }) + .required(), + test: Joi.object() + .keys({ + stages: Joi.array().items( + Joi.object().keys({ + action: Joi.string() + .valid('constantConcurrentUsers', 'rampConcurrentUsers') + .required(), + duration: Joi.string().pattern(SCALABILITY_DURATION_PATTERN).required(), + minUsersCount: Joi.number().when('action', { + is: 'rampConcurrentUsers', + then: Joi.number().required().less(Joi.ref('maxUsersCount')), + otherwise: Joi.forbidden(), + }), + maxUsersCount: Joi.number().required().greater(0), + }) + ), + }) + .required(), + maxDuration: Joi.string().pattern(SCALABILITY_DURATION_PATTERN).required(), + }) + .optional(), + // settings for the kibanaServer.uiSettings module uiSettings: Joi.object() .keys({ diff --git a/packages/shared-ux/page/analytics_no_data/src/services.tsx b/packages/shared-ux/page/analytics_no_data/src/services.tsx index 70ba29ed2f648..7868014749997 100644 --- a/packages/shared-ux/page/analytics_no_data/src/services.tsx +++ b/packages/shared-ux/page/analytics_no_data/src/services.tsx @@ -27,6 +27,8 @@ type DataView = unknown; interface DataViewEditorOptions { /** Handler to be invoked when the Data View Editor completes a save operation. */ onSave: (dataView: DataView) => void; + /** If set to false, will skip empty prompt in data view editor. */ + showEmptyPrompt?: boolean; } /** diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index df638e4b66bbe..4e36b691158e6 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -1,2008 +1,699 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CollapsibleNav renders links grouped by category 1`] = ` -} - 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/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts index 1a47ba7196253..048fb72af67be 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts @@ -19,6 +19,7 @@ import { sortOrderSchema } from './common_schemas'; * - nested * - reverse_nested * - terms + * - multi_terms * * Not fully supported: * - filter @@ -37,7 +38,6 @@ import { sortOrderSchema } from './common_schemas'; * - global * - ip_range * - missing - * - multi_terms * - parent * - range * - rare_terms @@ -63,6 +63,36 @@ const boolSchema = s.object({ }), }); +const orderSchema = s.oneOf([ + sortOrderSchema, + s.recordOf(s.string(), sortOrderSchema), + s.arrayOf(s.recordOf(s.string(), sortOrderSchema)), +]); + +const termsSchema = s.object({ + field: s.maybe(s.string()), + collect_mode: s.maybe(s.string()), + exclude: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])), + include: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])), + execution_hint: s.maybe(s.string()), + missing: s.maybe(s.number()), + min_doc_count: s.maybe(s.number({ min: 1 })), + size: s.maybe(s.number()), + show_term_doc_count_error: s.maybe(s.boolean()), + order: s.maybe(orderSchema), +}); + +const multiTermsSchema = s.object({ + terms: s.arrayOf(termsSchema), + size: s.maybe(s.number()), + shard_size: s.maybe(s.number()), + show_term_doc_count_error: s.maybe(s.boolean()), + min_doc_count: s.maybe(s.number()), + shard_min_doc_count: s.maybe(s.number()), + collect_mode: s.maybe(s.oneOf([s.literal('depth_first'), s.literal('breadth_first')])), + order: s.maybe(s.recordOf(s.string(), orderSchema)), +}); + export const bucketAggsSchemas: Record = { date_range: s.object({ field: s.string(), @@ -104,22 +134,6 @@ export const bucketAggsSchemas: Record = { reverse_nested: s.object({ path: s.maybe(s.string()), }), - terms: s.object({ - field: s.maybe(s.string()), - collect_mode: s.maybe(s.string()), - exclude: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])), - include: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])), - execution_hint: s.maybe(s.string()), - missing: s.maybe(s.number()), - min_doc_count: s.maybe(s.number({ min: 1 })), - size: s.maybe(s.number()), - show_term_doc_count_error: s.maybe(s.boolean()), - order: s.maybe( - s.oneOf([ - sortOrderSchema, - s.recordOf(s.string(), sortOrderSchema), - s.arrayOf(s.recordOf(s.string(), sortOrderSchema)), - ]) - ), - }), + multi_terms: multiTermsSchema, + terms: termsSchema, }; diff --git a/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts b/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts index 0296dd25b56ee..db50ab2b45d65 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts @@ -94,6 +94,28 @@ describe('validateAndConvertAggregations', () => { }); }); + it('validates multi_terms aggregations', () => { + expect( + validateAndConvertAggregations( + ['foo'], + { + aggName: { + multi_terms: { + terms: [{ field: 'foo.attributes.description' }, { field: 'foo.attributes.bytes' }], + }, + }, + }, + mockMappings + ) + ).toEqual({ + aggName: { + multi_terms: { + terms: [{ field: 'foo.description' }, { field: 'foo.bytes' }], + }, + }, + }); + }); + it('validates a nested field in simple aggregations', () => { expect( validateAndConvertAggregations( diff --git a/src/core/server/saved_objects/service/lib/aggregations/validation.ts b/src/core/server/saved_objects/service/lib/aggregations/validation.ts index 445d6b6a7ce22..76098d73306af 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/validation.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/validation.ts @@ -8,7 +8,7 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ObjectType } from '@kbn/config-schema'; -import { isPlainObject } from 'lodash'; +import { isPlainObject, isArray } from 'lodash'; import { IndexMapping } from '../../../mappings'; import { @@ -181,11 +181,17 @@ const recursiveRewrite = ( const nestedContext = childContext(context, key); const newKey = rewriteKey ? validateAndRewriteAttributePath(key, nestedContext) : key; - const newValue = rewriteValue - ? validateAndRewriteAttributePath(value, nestedContext) - : isPlainObject(value) - ? recursiveRewrite(value, nestedContext, [...parents, key]) - : value; + + let newValue = value; + if (rewriteValue) { + newValue = validateAndRewriteAttributePath(value, nestedContext); + } else if (isArray(value)) { + newValue = value.map((v) => + isPlainObject(v) ? recursiveRewrite(v, nestedContext, parents) : v + ); + } else if (isPlainObject(value)) { + newValue = recursiveRewrite(value, nestedContext, [...parents, key]); + } return { ...memo, diff --git a/src/dev/build/lib/integration_tests/download.test.ts b/src/dev/build/lib/integration_tests/download.test.ts index 4c5b7025da098..7046a831d84f0 100644 --- a/src/dev/build/lib/integration_tests/download.test.ts +++ b/src/dev/build/lib/integration_tests/download.test.ts @@ -196,7 +196,7 @@ describe('downloadToDisk', () => { retryDelaySecMultiplier: 0.1, }); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Request failed with status code 500]` + `[AxiosError: Request failed with status code 500]` ); expect(logWritter.messages).toMatchInlineSnapshot(` Array [ @@ -269,7 +269,7 @@ describe('downloadToString', () => { maxAttempts: 1, }); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Request failed with status code 200]` + `[AxiosError: Request failed with status code 200]` ); expect(logWritter.messages).toMatchInlineSnapshot(` Array [ 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/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts index 3a4a1fdb813fc..b8969fd599765 100644 --- a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -56,7 +56,6 @@ export const sampleLayer: DataLayerConfig = { splitAccessor: 'd', columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: createSampleDatatableWithRows([]), @@ -108,6 +107,8 @@ export const createArgsWithLayers = ( type: 'axisExtentConfig', }, layers: Array.isArray(layers) ? layers : [layers], + yLeftScale: 'linear', + yRightScale: 'linear', }); export function sampleArgs() { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts index 8c060ef4096d7..0c9085cce7664 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -7,7 +7,7 @@ */ import { ArgumentType } from '@kbn/expressions-plugin/common'; -import { SeriesTypes, XScaleTypes, YScaleTypes, Y_CONFIG } from '../constants'; +import { SeriesTypes, XScaleTypes, Y_CONFIG } from '../constants'; import { strings } from '../i18n'; import { DataLayerArgs, ExtendedDataLayerArgs } from '../types'; @@ -16,16 +16,15 @@ type CommonDataLayerFnArgs = { [key in keyof CommonDataLayerArgs]: ArgumentType; }; -export const commonDataLayerArgs: CommonDataLayerFnArgs = { +export const commonDataLayerArgs: Omit< + CommonDataLayerFnArgs, + 'accessors' | 'xAccessor' | 'splitAccessor' +> = { hide: { types: ['boolean'], default: false, help: strings.getHideHelp(), }, - xAccessor: { - types: ['string'], - help: strings.getXAccessorHelp(), - }, seriesType: { aliases: ['_'], types: ['string'], @@ -45,21 +44,6 @@ export const commonDataLayerArgs: CommonDataLayerFnArgs = { default: false, help: strings.getIsHistogramHelp(), }, - yScaleType: { - options: [...Object.values(YScaleTypes)], - help: strings.getYScaleTypeHelp(), - default: YScaleTypes.LINEAR, - strict: true, - }, - splitAccessor: { - types: ['string'], - help: strings.getSplitAccessorHelp(), - }, - accessors: { - types: ['string'], - help: strings.getAccessorsHelp(), - multi: true, - }, yConfig: { types: [Y_CONFIG], help: strings.getYConfigHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts index f338e08a88940..d85f5ae2b2f77 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts @@ -12,12 +12,7 @@ import { ReferenceLineLayerFn, ExtendedReferenceLineLayerFn } from '../types'; type CommonReferenceLineLayerFn = ReferenceLineLayerFn | ExtendedReferenceLineLayerFn; -export const commonReferenceLineLayerArgs: CommonReferenceLineLayerFn['args'] = { - accessors: { - types: ['string'], - help: strings.getRLAccessorsHelp(), - multi: true, - }, +export const commonReferenceLineLayerArgs: Omit = { yConfig: { types: [EXTENDED_Y_CONFIG], help: strings.getRLYConfigHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts index f80d814571076..0dbe71ef554cc 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts @@ -17,6 +17,7 @@ import { TICK_LABELS_CONFIG, ValueLabelModes, XYCurveTypes, + YScaleTypes, } from '../constants'; import { strings } from '../i18n'; import { LayeredXyVisFn, XyVisFn } from '../types'; @@ -46,6 +47,18 @@ export const commonXYArgs: CommonXYFn['args'] = { help: strings.getYRightExtentHelp(), default: `{${AXIS_EXTENT_CONFIG}}`, }, + yLeftScale: { + options: [...Object.values(YScaleTypes)], + help: strings.getYLeftScaleTypeHelp(), + default: YScaleTypes.LINEAR, + strict: true, + }, + yRightScale: { + options: [...Object.values(YScaleTypes)], + help: strings.getYRightScaleTypeHelp(), + default: YScaleTypes.LINEAR, + strict: true, + }, legend: { types: [LEGEND_CONFIG], help: strings.getLegendHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts index 4eb50c3388ec1..a7aa63645d119 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts @@ -7,10 +7,9 @@ */ import { ExtendedDataLayerFn } from '../types'; -import { EXTENDED_DATA_LAYER, LayerTypes } from '../constants'; +import { EXTENDED_DATA_LAYER } from '../constants'; import { strings } from '../i18n'; import { commonDataLayerArgs } from './common_data_layer_args'; -import { getAccessors } from '../helpers'; export const extendedDataLayerFunction: ExtendedDataLayerFn = { name: EXTENDED_DATA_LAYER, @@ -20,6 +19,19 @@ export const extendedDataLayerFunction: ExtendedDataLayerFn = { inputTypes: ['datatable'], args: { ...commonDataLayerArgs, + xAccessor: { + types: ['string'], + help: strings.getXAccessorHelp(), + }, + splitAccessor: { + types: ['string'], + help: strings.getSplitAccessorHelp(), + }, + accessors: { + types: ['string'], + help: strings.getAccessorsHelp(), + multi: true, + }, table: { types: ['datatable'], help: strings.getTableHelp(), @@ -29,14 +41,8 @@ export const extendedDataLayerFunction: ExtendedDataLayerFn = { help: strings.getLayerIdHelp(), }, }, - fn(input, args) { - const table = args.table ?? input; - return { - type: EXTENDED_DATA_LAYER, - ...args, - ...getAccessors(args, table), - layerType: LayerTypes.DATA, - table, - }; + async fn(input, args, context) { + const { extendedDataLayerFn } = await import('./extended_data_layer_fn'); + return await extendedDataLayerFn(input, args, context); }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts new file mode 100644 index 0000000000000..c4d714f11ddd9 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer_fn.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; +import { ExtendedDataLayerArgs, ExtendedDataLayerFn } from '../types'; +import { EXTENDED_DATA_LAYER, LayerTypes } from '../constants'; +import { getAccessors } from '../helpers'; + +export const extendedDataLayerFn: ExtendedDataLayerFn['fn'] = async (data, args, context) => { + const table = args.table ?? data; + const accessors = getAccessors(args, table); + + validateAccessor(accessors.xAccessor, table.columns); + validateAccessor(accessors.splitAccessor, table.columns); + accessors.accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); + + return { + type: EXTENDED_DATA_LAYER, + ...args, + layerType: LayerTypes.DATA, + ...accessors, + table, + }; +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts index 4f75838bea114..41b264cf53a4d 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; import { LayerTypes, EXTENDED_REFERENCE_LINE_LAYER } from '../constants'; import { ExtendedReferenceLineLayerFn } from '../types'; import { strings } from '../i18n'; @@ -19,6 +20,11 @@ export const extendedReferenceLineLayerFunction: ExtendedReferenceLineLayerFn = inputTypes: ['datatable'], args: { ...commonReferenceLineLayerArgs, + accessors: { + types: ['string'], + help: strings.getRLAccessorsHelp(), + multi: true, + }, table: { types: ['datatable'], help: strings.getTableHelp(), @@ -29,12 +35,16 @@ export const extendedReferenceLineLayerFunction: ExtendedReferenceLineLayerFn = }, }, fn(input, args) { + const table = args.table ?? input; + const accessors = args.accessors ?? []; + accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); + return { type: EXTENDED_REFERENCE_LINE_LAYER, ...args, accessors: args.accessors ?? [], layerType: LayerTypes.REFERENCELINE, - table: args.table ?? input, + table, }; }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts index 9c6e27c958530..04c06f92d616f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { validateAccessor } from '@kbn/visualizations-plugin/common/utils'; import { LayerTypes, REFERENCE_LINE_LAYER } from '../constants'; import { ReferenceLineLayerFn } from '../types'; import { strings } from '../i18n'; @@ -17,13 +18,23 @@ export const referenceLineLayerFunction: ReferenceLineLayerFn = { type: REFERENCE_LINE_LAYER, help: strings.getRLHelp(), inputTypes: ['datatable'], - args: { ...commonReferenceLineLayerArgs }, + args: { + ...commonReferenceLineLayerArgs, + accessors: { + types: ['string', 'vis_dimension'], + help: strings.getRLAccessorsHelp(), + multi: true, + }, + }, fn(table, args) { + const accessors = args.accessors ?? []; + accessors.forEach((accessor) => validateAccessor(accessor, table.columns)); + return { type: REFERENCE_LINE_LAYER, ...args, - accessors: args.accessors ?? [], layerType: LayerTypes.REFERENCELINE, + accessors, table, }; }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts index d2aee5048deb3..e4e519b0a7433 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts @@ -20,6 +20,19 @@ export const xyVisFunction: XyVisFn = { args: { ...commonXYArgs, ...commonDataLayerArgs, + xAccessor: { + types: ['string', 'vis_dimension'], + help: strings.getXAccessorHelp(), + }, + splitAccessor: { + types: ['string', 'vis_dimension'], + help: strings.getSplitAccessorHelp(), + }, + accessors: { + types: ['string', 'vis_dimension'], + help: strings.getAccessorsHelp(), + multi: true, + }, referenceLineLayers: { types: [REFERENCE_LINE_LAYER], help: strings.getReferenceLineLayerHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index ff217a965fae9..04d0b954c5d5c 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -12,6 +12,7 @@ import { validateAccessor, } from '@kbn/visualizations-plugin/common/utils'; import type { Datatable } from '@kbn/expressions-plugin/common'; +import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER } from '../constants'; import { appendLayerIds, getAccessors } from '../helpers'; import { DataLayerConfigResult, XYLayerConfig, XyVisFn, XYArgs } from '../types'; @@ -30,14 +31,13 @@ const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult seriesType: args.seriesType, hide: args.hide, columnToLabel: args.columnToLabel, - yScaleType: args.yScaleType, xScaleType: args.xScaleType, isHistogram: args.isHistogram, palette: args.palette, yConfig: args.yConfig, layerType: LayerTypes.DATA, table, - ...getAccessors(args, table), + ...getAccessors(args, table), }); export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { @@ -54,7 +54,6 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { hide, splitAccessor, columnToLabel, - yScaleType, xScaleType, isHistogram, yConfig, @@ -64,6 +63,10 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { const dataLayers: DataLayerConfigResult[] = [createDataLayer(args, data)]; + validateAccessor(dataLayers[0].xAccessor, data.columns); + validateAccessor(dataLayers[0].splitAccessor, data.columns); + dataLayers[0].accessors.forEach((accessor) => validateAccessor(accessor, data.columns)); + const layers: XYLayerConfig[] = [ ...appendLayerIds(dataLayers, 'dataLayers'), ...appendLayerIds(referenceLineLayers, 'referenceLineLayers'), diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts index b07a2d2e2a02e..667b2697e480f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts @@ -7,7 +7,7 @@ */ import { Datatable, PointSeriesColumnNames } from '@kbn/expressions-plugin/common'; -import { WithLayerId, DataLayerArgs } from '../types'; +import { WithLayerId } from '../types'; function isWithLayerId(layer: T): layer is T & WithLayerId { return (layer as T & WithLayerId).layerId ? true : false; @@ -27,10 +27,13 @@ export function appendLayerIds( })); } -export function getAccessors(args: DataLayerArgs, table: Datatable) { - let splitAccessor = args.splitAccessor; - let xAccessor = args.xAccessor; - let accessors = args.accessors ?? []; +export function getAccessors( + args: U, + table: Datatable +) { + let splitAccessor: T | string | undefined = args.splitAccessor; + let xAccessor: T | string | undefined = args.xAccessor; + let accessors: Array = args.accessors ?? []; if (!splitAccessor && !xAccessor && !(accessors && accessors.length)) { const y = table.columns.find((column) => column.id === PointSeriesColumnNames.Y)?.id; xAccessor = table.columns.find((column) => column.id === PointSeriesColumnNames.X)?.id; diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index 5b5906ac71582..a69f0034b9e23 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -49,6 +49,14 @@ export const strings = { i18n.translate('expressionXY.xyVis.yRightExtent.help', { defaultMessage: 'Y right axis extents', }), + getYLeftScaleTypeHelp: () => + i18n.translate('expressionXY.xyVis.yLeftScaleType.help', { + defaultMessage: 'The scale type of the left y axis', + }), + getYRightScaleTypeHelp: () => + i18n.translate('expressionXY.xyVis.yRightScaleType.help', { + defaultMessage: 'The scale type of the right y axis', + }), getLegendHelp: () => i18n.translate('expressionXY.xyVis.legend.help', { defaultMessage: 'Configure the chart legend.', @@ -149,10 +157,6 @@ export const strings = { i18n.translate('expressionXY.dataLayer.isHistogram.help', { defaultMessage: 'Whether to layout the chart as a histogram', }), - getYScaleTypeHelp: () => - i18n.translate('expressionXY.dataLayer.yScaleType.help', { - defaultMessage: 'The scale type of the y axes', - }), getSplitAccessorHelp: () => i18n.translate('expressionXY.dataLayer.splitAccessor.help', { defaultMessage: 'The column to split by', diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 174b3e85f3686..375ee584f380c 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -12,8 +12,7 @@ import type { PaletteOutput } from '@kbn/coloring'; import { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin'; import { LegendSize } from '@kbn/visualizations-plugin/public'; import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common'; -import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; - +import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { AxisExtentModes, FillStyles, @@ -96,13 +95,12 @@ export interface YConfig { } export interface DataLayerArgs { - accessors: string[]; + accessors: Array; seriesType: SeriesType; - xAccessor?: string; + xAccessor?: string | ExpressionValueVisDimension; hide?: boolean; - splitAccessor?: string; + splitAccessor?: string | ExpressionValueVisDimension; columnToLabel?: string; // Actually a JSON key-value pair - yScaleType: YScaleType; xScaleType: XScaleType; isHistogram: boolean; palette: PaletteOutput; @@ -121,7 +119,6 @@ export interface ExtendedDataLayerArgs { hide?: boolean; splitAccessor?: string; columnToLabel?: string; // Actually a JSON key-value pair - yScaleType: YScaleType; xScaleType: XScaleType; isHistogram: boolean; palette: PaletteOutput; @@ -188,6 +185,8 @@ export interface XYArgs extends DataLayerArgs { yRightTitle: string; yLeftExtent: AxisExtentConfigResult; yRightExtent: AxisExtentConfigResult; + yLeftScale: YScaleType; + yRightScale: YScaleType; legend: LegendConfigResult; endValue?: EndValue; emphasizeFitting?: boolean; @@ -214,6 +213,8 @@ export interface LayeredXYArgs { yRightTitle: string; yLeftExtent: AxisExtentConfigResult; yRightExtent: AxisExtentConfigResult; + yLeftScale: YScaleType; + yRightScale: YScaleType; legend: LegendConfigResult; endValue?: EndValue; emphasizeFitting?: boolean; @@ -237,6 +238,8 @@ export interface XYProps { yRightTitle: string; yLeftExtent: AxisExtentConfigResult; yRightExtent: AxisExtentConfigResult; + yLeftScale: YScaleType; + yRightScale: YScaleType; legend: LegendConfigResult; endValue?: EndValue; emphasizeFitting?: boolean; @@ -276,7 +279,7 @@ export type ExtendedAnnotationLayerConfigResult = ExtendedAnnotationLayerArgs & }; export interface ReferenceLineLayerArgs { - accessors: string[]; + accessors: Array; columnToLabel?: string; yConfig?: ExtendedYConfigResult[]; } @@ -385,7 +388,7 @@ export type ExtendedDataLayerFn = ExpressionFunctionDefinition< typeof EXTENDED_DATA_LAYER, Datatable, ExtendedDataLayerArgs, - ExtendedDataLayerConfigResult + Promise >; export type ReferenceLineLayerFn = ExpressionFunctionDefinition< diff --git a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx index e84d8c001fb82..77ce5ee76ebbf 100644 --- a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx @@ -164,7 +164,6 @@ export const dateHistogramLayer: DataLayerConfig = { layerType: LayerTypes.DATA, hide: false, xAccessor: 'xAccessorId', - yScaleType: 'linear', xScaleType: 'time', isHistogram: true, splitAccessor: 'splitAccessorId', diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index 3eeeee402205a..0bc41100012de 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -766,7 +766,6 @@ exports[`XYChart component it renders area 1`] = ` "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -807,6 +806,7 @@ exports[`XYChart component it renders area 1`] = ` }, "groupId": "left", "position": "left", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -1310,7 +1310,6 @@ exports[`XYChart component it renders bar 1`] = ` "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -1351,6 +1350,7 @@ exports[`XYChart component it renders bar 1`] = ` }, "groupId": "left", "position": "left", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -1854,7 +1854,6 @@ exports[`XYChart component it renders horizontal bar 1`] = ` "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -1895,6 +1894,7 @@ exports[`XYChart component it renders horizontal bar 1`] = ` }, "groupId": "left", "position": "bottom", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -2398,7 +2398,6 @@ exports[`XYChart component it renders line 1`] = ` "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -2439,6 +2438,7 @@ exports[`XYChart component it renders line 1`] = ` }, "groupId": "left", "position": "left", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -2942,7 +2942,6 @@ exports[`XYChart component it renders stacked area 1`] = ` "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -2983,6 +2982,7 @@ exports[`XYChart component it renders stacked area 1`] = ` }, "groupId": "left", "position": "left", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -3486,7 +3486,6 @@ exports[`XYChart component it renders stacked bar 1`] = ` "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -3527,6 +3526,7 @@ exports[`XYChart component it renders stacked bar 1`] = ` }, "groupId": "left", "position": "left", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -4030,7 +4030,6 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = ` "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -4071,6 +4070,7 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = ` }, "groupId": "left", "position": "bottom", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -4829,7 +4829,6 @@ exports[`XYChart component split chart should render split chart if both, splitR "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -4870,6 +4869,7 @@ exports[`XYChart component split chart should render split chart if both, splitR }, "groupId": "left", "position": "left", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -5627,7 +5627,6 @@ exports[`XYChart component split chart should render split chart if splitColumnA "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -5668,6 +5667,7 @@ exports[`XYChart component split chart should render split chart if splitColumnA }, "groupId": "left", "position": "left", + "scale": "linear", "series": Array [ Object { "accessor": "a", @@ -6425,7 +6425,6 @@ exports[`XYChart component split chart should render split chart if splitRowAcce "type": "dataLayer", "xAccessor": "c", "xScaleType": "ordinal", - "yScaleType": "linear", }, ] } @@ -6466,6 +6465,7 @@ exports[`XYChart component split chart should render split chart if splitRowAcce }, "groupId": "left", "position": "left", + "scale": "linear", "series": Array [ Object { "accessor": "a", diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index fa2c081f08700..842baeb82d78d 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -34,7 +34,6 @@ import type { CommonXYAnnotationLayerConfig, CollectiveConfig, } from '../../common'; - import { AnnotationIcon, hasIcon, Marker, MarkerBody } from '../helpers'; import { mapVerticalToHorizontalPlacement, LINES_MARKER_SIZE } from '../helpers'; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx index 1166d41a9e402..b7a366c7b3109 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx @@ -15,6 +15,8 @@ import { import React, { FC } from 'react'; import { PaletteRegistry } from '@kbn/coloring'; import { FormatFactory } from '@kbn/field-formats-plugin/common'; +import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; + import { CommonXYDataLayerConfig, EndValue, @@ -71,7 +73,8 @@ export const DataLayers: FC = ({ <> {layers.flatMap((layer) => layer.accessors.map((accessor, accessorIndex) => { - const { seriesType, columnToLabel, layerId } = layer; + const { seriesType, columnToLabel, layerId, table } = layer; + const yColumnId = getAccessorByDimension(accessor, table.columns); const columnToLabelMap: Record = columnToLabel ? JSON.parse(columnToLabel) : {}; @@ -84,12 +87,12 @@ export const DataLayers: FC = ({ const isPercentage = seriesType.includes('percentage'); const yAxis = yAxesConfiguration.find((axisConfiguration) => - axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor) + axisConfiguration.series.find((currentSeries) => currentSeries.accessor === yColumnId) ); const seriesProps = getSeriesProps({ layer, - accessor, + accessor: yColumnId, chartHasMoreThanOneBarSeries, colorAssignments, formatFactory, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx index 78ac1ed8d10cf..8289d605aa913 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx @@ -162,7 +162,6 @@ const sampleLayer: DataLayerConfig = { splitAccessor: 'splitAccessorId', columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx index fd7e905eff881..68e5b89559933 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts'; +import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import type { FilterEvent } from '../types'; import type { CommonXYDataLayerConfig } from '../../common'; import type { FormatFactory } from '../types'; @@ -23,7 +24,11 @@ export const getLegendAction = ( React.memo(({ series: [xySeries] }) => { const series = xySeries as XYChartSeriesIdentifier; const layerIndex = dataLayers.findIndex((l) => - series.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString())) + series.seriesKeys.some((key: string | number) => + l.accessors.some( + (accessor) => getAccessorByDimension(accessor, l.table.columns) === key.toString() + ) + ) ); if (layerIndex === -1) { @@ -36,11 +41,12 @@ export const getLegendAction = ( } const splitLabel = series.seriesKeys[0] as string; - const accessor = layer.splitAccessor; const { table } = layer; - const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor); - const formatter = formatFactory(splitColumn && getFormat(splitColumn.meta)); + const accessor = getAccessorByDimension(layer.splitAccessor, table.columns); + const formatter = formatFactory( + accessor ? getFormat(table.columns, layer.splitAccessor) : undefined + ); const rowIndex = table.rows.findIndex((row) => { if (formattedDatatables[layer.layerId]?.formattedColumns[accessor]) { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx b/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx index 78b6ef91926a8..10b2140eae6a1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx @@ -6,11 +6,15 @@ * Side Public License, v 1. */ -import { uniq } from 'lodash'; +import { isUndefined, uniq } from 'lodash'; import React from 'react'; import moment from 'moment'; import { Endzones } from '@kbn/charts-plugin/public'; import { search } from '@kbn/data-plugin/public'; +import { + getAccessorByDimension, + getColumnByAccessor, +} from '@kbn/visualizations-plugin/common/utils'; import type { CommonXYDataLayerConfig } from '../../common'; export interface XDomain { @@ -22,7 +26,7 @@ export interface XDomain { export const getAppliedTimeRange = (layers: CommonXYDataLayerConfig[]) => { return layers .map(({ xAccessor, table }) => { - const xColumn = table.columns.find((col) => col.id === xAccessor); + const xColumn = xAccessor ? getColumnByAccessor(xAccessor, table.columns) : null; const timeRange = xColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.timeRange; if (timeRange) { @@ -57,9 +61,11 @@ export const getXDomain = ( if (isHistogram && isFullyQualified(baseDomain)) { const xValues = uniq( layers - .flatMap(({ table, xAccessor }) => - table.rows.map((row) => row[xAccessor!].valueOf()) - ) + .flatMap(({ table, xAccessor }) => { + const accessor = xAccessor && getAccessorByDimension(xAccessor, table.columns); + return table.rows.map((row) => accessor && row[accessor] && row[accessor].valueOf()); + }) + .filter((v) => !isUndefined(v)) .sort() ); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 7f6dcb7a73925..911dbeea10416 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -157,7 +157,6 @@ describe('XYChart component', () => { splitAccessor: 'd', columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'time', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: { @@ -254,7 +253,6 @@ describe('XYChart component', () => { splitAccessor: 'd', columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'time', - yScaleType: 'linear', isHistogram: true, palette: mockPaletteOutput, table: data, @@ -847,7 +845,6 @@ describe('XYChart component', () => { layerType: LayerTypes.DATA, hide: false, xAccessor: 'xAccessorId', - yScaleType: 'linear', xScaleType: 'linear', isHistogram: true, seriesType: 'bar_stacked', @@ -923,7 +920,6 @@ describe('XYChart component', () => { isHistogram: true, seriesType: 'bar_stacked', xAccessor: 'b', - yScaleType: 'linear', xScaleType: 'time', splitAccessor: 'b', accessors: ['d'], @@ -1045,7 +1041,6 @@ describe('XYChart component', () => { layerType: LayerTypes.DATA, hide: false, xAccessor: 'xAccessorId', - yScaleType: 'linear', xScaleType: 'linear', isHistogram: true, seriesType: 'bar_stacked', @@ -1125,7 +1120,6 @@ describe('XYChart component', () => { accessors: ['a', 'b'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: data, @@ -1184,7 +1178,6 @@ describe('XYChart component', () => { accessors: ['a', 'b'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: newData, @@ -1215,7 +1208,6 @@ describe('XYChart component', () => { accessors: ['a', 'b'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: data, @@ -1898,7 +1890,7 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - layers: [{ ...(args.layers[0] as DataLayerConfig), yScaleType: 'sqrt' }], + yLeftScale: 'sqrt', }} /> ); @@ -2107,6 +2099,8 @@ describe('XYChart component', () => { xTitle: '', yTitle: '', yRightTitle: '', + yLeftScale: 'linear', + yRightScale: 'linear', legend: { type: 'legendConfig', isVisible: false, position: Position.Top }, valueLabels: 'hide', tickLabelsVisibilitySettings: { @@ -2146,7 +2140,6 @@ describe('XYChart component', () => { splitAccessor: 'b', columnToLabel: '', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: data1, @@ -2161,7 +2154,6 @@ describe('XYChart component', () => { splitAccessor: 'b', columnToLabel: '', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: data2, @@ -2224,6 +2216,8 @@ describe('XYChart component', () => { mode: 'full', type: 'axisExtentConfig', }, + yLeftScale: 'linear', + yRightScale: 'linear', layers: [ { layerId: 'first', @@ -2235,7 +2229,6 @@ describe('XYChart component', () => { splitAccessor: 'b', columnToLabel: '', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: data, @@ -2296,6 +2289,8 @@ describe('XYChart component', () => { mode: 'full', type: 'axisExtentConfig', }, + yLeftScale: 'linear', + yRightScale: 'linear', layers: [ { layerId: 'first', @@ -2307,7 +2302,6 @@ describe('XYChart component', () => { splitAccessor: 'b', columnToLabel: '', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: data, @@ -2557,7 +2551,6 @@ describe('XYChart component', () => { xAccessor: 'c', accessors: ['a', 'b'], xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, table: data, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index d8ea89def7128..7b31112c4b9ed 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -33,6 +33,10 @@ import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { ChartsPluginSetup, ChartsPluginStart, useActiveCursor } from '@kbn/charts-plugin/public'; import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; +import { + getAccessorByDimension, + getColumnByAccessor, +} from '@kbn/visualizations-plugin/common/utils'; import { DEFAULT_LEGEND_SIZE, LegendSizeToPixels, @@ -153,6 +157,8 @@ export function XYChart({ yLeftExtent, yRightExtent, valuesInLegend, + yLeftScale, + yRightScale, splitColumnAccessor, splitRowAccessor, } = args; @@ -184,9 +190,15 @@ export function XYChart({ } // use formatting hint of first x axis column to format ticks - const xAxisColumn = dataLayers[0]?.table.columns.find(({ id }) => id === dataLayers[0].xAccessor); - - const xAxisFormatter = formatFactory(xAxisColumn && getFormat(xAxisColumn.meta)); + const xAxisColumn = dataLayers[0].xAccessor + ? getColumnByAccessor(dataLayers[0].xAccessor, dataLayers[0]?.table.columns) + : undefined; + + const xAxisFormatter = formatFactory( + dataLayers[0].xAccessor + ? getFormat(dataLayers[0].table.columns, dataLayers[0].xAccessor) + : undefined + ); // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers const safeXAccessorLabelRenderer = (value: unknown): string => @@ -200,7 +212,13 @@ export function XYChart({ filteredLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor); const shouldRotate = isHorizontalChart(dataLayers); - const yAxesConfiguration = getAxesConfiguration(dataLayers, shouldRotate, formatFactory); + const yAxesConfiguration = getAxesConfiguration( + dataLayers, + shouldRotate, + formatFactory, + yLeftScale, + yRightScale + ); const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name); const axisTitlesVisibilitySettings = args.axisTitlesVisibilitySettings || { @@ -257,12 +275,14 @@ export function XYChart({ const annotationsLayers = getAnnotationsLayers(layers); const firstTable = dataLayers[0]?.table; - const xColumnId = firstTable?.columns.find((col) => col.id === dataLayers[0]?.xAccessor)?.id; + const columnId = dataLayers[0]?.xAccessor + ? getColumnByAccessor(dataLayers[0]?.xAccessor, firstTable.columns)?.id + : null; const groupedLineAnnotations = getAnnotationsGroupedByInterval( annotationsLayers, minInterval, - xColumnId ? firstTable.rows[0]?.[xColumnId] : undefined, + columnId ? firstTable.rows[0]?.[columnId] : undefined, xAxisFormatter ); const rangeAnnotations = getRangeAnnotations(annotationsLayers); @@ -370,7 +390,11 @@ export function XYChart({ const xyGeometry = geometry as GeometryValue; const layerIndex = dataLayers.findIndex((l) => - xySeries.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString())) + xySeries.seriesKeys.some((key: string | number) => + l.accessors.some( + (accessor) => getAccessorByDimension(accessor, l.table.columns) === key.toString() + ) + ) ); if (layerIndex === -1) { @@ -380,48 +404,53 @@ export function XYChart({ const layer = dataLayers[layerIndex]; const { table } = layer; - const xColumn = table.columns.find((col) => col.id === layer.xAccessor); + const xColumn = layer.xAccessor && getColumnByAccessor(layer.xAccessor, table.columns); + const xAccessor = layer.xAccessor + ? getAccessorByDimension(layer.xAccessor, table.columns) + : undefined; const currentXFormatter = - layer.xAccessor && - formattedDatatables[layer.layerId]?.formattedColumns[layer.xAccessor] && - xColumn - ? formatFactory(getFormat(xColumn.meta)) + xAccessor && formattedDatatables[layer.layerId]?.formattedColumns[xAccessor] && xColumn + ? formatFactory(layer.xAccessor ? getFormat(table.columns, layer.xAccessor) : undefined) : xAxisFormatter; const rowIndex = table.rows.findIndex((row) => { - if (layer.xAccessor) { - if (formattedDatatables[layer.layerId]?.formattedColumns[layer.xAccessor]) { + if (xAccessor) { + if (formattedDatatables[layer.layerId]?.formattedColumns[xAccessor]) { // stringify the value to compare with the chart value - return currentXFormatter.convert(row[layer.xAccessor]) === xyGeometry.x; + return currentXFormatter.convert(row[xAccessor]) === xyGeometry.x; } - return row[layer.xAccessor] === xyGeometry.x; + return row[xAccessor] === xyGeometry.x; } }); const points = [ { row: rowIndex, - column: table.columns.findIndex((col) => col.id === layer.xAccessor), - value: layer.xAccessor ? table.rows[rowIndex][layer.xAccessor] : xyGeometry.x, + column: table.columns.findIndex((col) => col.id === xAccessor), + value: xAccessor ? table.rows[rowIndex][xAccessor] : xyGeometry.x, }, ]; if (xySeries.seriesKeys.length > 1) { const pointValue = xySeries.seriesKeys[0]; + const splitAccessor = layer.splitAccessor + ? getAccessorByDimension(layer.splitAccessor, table.columns) + : undefined; - const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor); - const splitFormatter = formatFactory(splitColumn && getFormat(splitColumn.meta)); + const splitFormatter = formatFactory( + layer.splitAccessor ? getFormat(table.columns, layer.splitAccessor) : undefined + ); points.push({ row: table.rows.findIndex((row) => { - if (layer.splitAccessor) { - if (formattedDatatables[layer.layerId]?.formattedColumns[layer.splitAccessor]) { - return splitFormatter.convert(row[layer.splitAccessor]) === pointValue; + if (splitAccessor) { + if (formattedDatatables[layer.layerId]?.formattedColumns[splitAccessor]) { + return splitFormatter.convert(row[splitAccessor]) === pointValue; } - return row[layer.splitAccessor] === pointValue; + return row[splitAccessor] === pointValue; } }), - column: table.columns.findIndex((col) => col.id === layer.splitAccessor), + column: table.columns.findIndex((col) => col.id === splitAccessor), value: pointValue, }); } @@ -441,8 +470,9 @@ export function XYChart({ } const { table } = dataLayers[0]; - - const xAxisColumnIndex = table.columns.findIndex((el) => el.id === dataLayers[0].xAccessor); + const xAccessor = + dataLayers[0].xAccessor && getAccessorByDimension(dataLayers[0].xAccessor, table.columns); + const xAxisColumnIndex = table.columns.findIndex((el) => el.id === xAccessor); const context: BrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex }; onSelectRange(context); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts index f3abf76b2d05a..7f1f8b62b75a7 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts @@ -230,7 +230,6 @@ describe('axes_configuration', () => { splitAccessor: 'd', columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', - yScaleType: 'linear', isHistogram: false, palette: { type: 'palette', name: 'default' }, table: tables.first, diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts index 710199f32f42b..89dc87ae5383b 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts @@ -7,8 +7,15 @@ */ import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; +import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import { FormatFactory } from '../types'; -import { AxisExtentConfig, CommonXYDataLayerConfig, ExtendedYConfig, YConfig } from '../../common'; +import { + AxisExtentConfig, + CommonXYDataLayerConfig, + ExtendedYConfig, + YConfig, + YScaleType, +} from '../../common'; import { isDataLayer } from './visualization'; import { getFormat } from './format'; @@ -26,6 +33,7 @@ export type GroupsConfiguration = Array<{ position: 'left' | 'right' | 'bottom' | 'top'; formatter?: IFieldFormat; series: Series[]; + scale?: YScaleType; }>; export function isFormatterCompatible( @@ -52,10 +60,12 @@ export function groupAxesByType(layers: CommonXYDataLayerConfig[]) { const { table } = layer; layer.accessors.forEach((accessor) => { const yConfig: Array | undefined = layer.yConfig; + const yAccessor = getAccessorByDimension(accessor, table?.columns || []); const mode = - yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || 'auto'; - const col = table.columns?.find((column) => column.id === accessor); - let formatter: SerializedFieldFormat = col?.meta ? getFormat(col.meta) : { id: 'number' }; + yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === yAccessor)?.axisMode || 'auto'; + let formatter: SerializedFieldFormat = getFormat(table.columns, accessor) || { + id: 'number', + }; if ( isDataLayer(layer) && layer.seriesType.includes('percentage') && @@ -70,7 +80,7 @@ export function groupAxesByType(layers: CommonXYDataLayerConfig[]) { } series[mode].push({ layer: layer.layerId, - accessor, + accessor: yAccessor, fieldFormat: formatter, }); }); @@ -107,7 +117,9 @@ export function groupAxesByType(layers: CommonXYDataLayerConfig[]) { export function getAxesConfiguration( layers: CommonXYDataLayerConfig[], shouldRotate: boolean, - formatFactory?: FormatFactory + formatFactory?: FormatFactory, + yLeftScale?: YScaleType, + yRightScale?: YScaleType ): GroupsConfiguration { const series = groupAxesByType(layers); @@ -119,6 +131,7 @@ export function getAxesConfiguration( position: shouldRotate ? 'bottom' : 'left', formatter: formatFactory?.(series.left[0].fieldFormat), series: series.left.map(({ fieldFormat, ...currentSeries }) => currentSeries), + scale: yLeftScale, }); } @@ -128,6 +141,7 @@ export function getAxesConfiguration( position: shouldRotate ? 'top' : 'right', formatter: formatFactory?.(series.right[0].fieldFormat), series: series.right.map(({ fieldFormat, ...currentSeries }) => currentSeries), + scale: yRightScale, }); } diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts index 8b1bdeeadb834..836d7209a6a5b 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts @@ -52,7 +52,6 @@ describe('color_assignment', () => { { layerId: 'first', type: 'dataLayer', - yScaleType: 'linear', xScaleType: 'linear', isHistogram: true, seriesType: 'bar', @@ -65,7 +64,6 @@ describe('color_assignment', () => { { layerId: 'second', type: 'dataLayer', - yScaleType: 'linear', xScaleType: 'linear', isHistogram: true, seriesType: 'bar', diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts index 4d141757a225b..0b7f8d8b08f22 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts @@ -8,6 +8,7 @@ import { uniq, mapValues } from 'lodash'; import { euiLightVars } from '@kbn/ui-theme'; +import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import { FormatFactory } from '../types'; import { isDataLayer } from './visualization'; import { CommonXYDataLayerConfig, CommonXYLayerConfig } from '../../common'; @@ -48,9 +49,10 @@ export function getColorAssignments( if (!layer.splitAccessor) { return { numberOfSeries: layer.accessors.length, splits: [] }; } - const splitAccessor = layer.splitAccessor; + const splitAccessor = getAccessorByDimension(layer.splitAccessor, layer.table.columns); const column = layer.table.columns?.find(({ id }) => id === splitAccessor); - const columnFormatter = column && formatFactory(getFormat(column.meta)); + const columnFormatter = + column && formatFactory(getFormat(layer.table.columns, layer.splitAccessor)); const splits = !column || !layer.table ? [] @@ -88,7 +90,9 @@ export function getColorAssignments( (sortedLayer.splitAccessor && splitRank !== -1 ? splitRank * sortedLayer.accessors.length : 0) + - sortedLayer.accessors.indexOf(yAccessor) + sortedLayer.accessors.findIndex( + (accessor) => getAccessorByDimension(accessor, sortedLayer.table.columns) === yAccessor + ) ); }, }; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index 2f117c542df16..c2a7c847e150b 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -24,6 +24,11 @@ import { SerializedFieldFormat, } from '@kbn/field-formats-plugin/common'; import { Datatable } from '@kbn/expressions-plugin'; +import { + getFormatByAccessor, + getAccessorByDimension, +} from '@kbn/visualizations-plugin/common/utils'; +import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { PaletteRegistry, SeriesLayer } from '@kbn/coloring'; import { CommonXYDataLayerConfig, XScaleType } from '../../common'; import { FormatFactory } from '../types'; @@ -53,7 +58,8 @@ type GetSeriesPropsFn = (config: { type GetSeriesNameFn = ( data: XYChartSeriesIdentifier, config: { - layer: CommonXYDataLayerConfig; + splitColumnId?: string; + accessorsCount: number; splitHint: SerializedFieldFormat | undefined; splitFormatter: FieldFormat; alreadyFormattedColumns: Record; @@ -112,11 +118,21 @@ export const getFormattedRow = ( export const getFormattedTable = ( table: Datatable, formatFactory: FormatFactory, - xAccessor: string | undefined, + xAccessor: string | ExpressionValueVisDimension | undefined, + accessors: Array, xScaleType: XScaleType ): { table: Datatable; formattedColumns: Record } => { const columnsFormatters = table.columns.reduce>( - (formatters, { id, meta }) => ({ ...formatters, [id]: formatFactory(getFormat(meta)) }), + (formatters, { id, meta }) => { + const accessor: string | ExpressionValueVisDimension | undefined = accessors.find( + (a) => getAccessorByDimension(a, table.columns) === id + ); + + return { + ...formatters, + [id]: formatFactory(accessor ? getFormat(table.columns, accessor) : meta.params), + }; + }, {} ); @@ -129,7 +145,7 @@ export const getFormattedTable = ( row, table.columns, columnsFormatters, - xAccessor, + xAccessor ? getAccessorByDimension(xAccessor, table.columns) : undefined, xScaleType ); return { @@ -154,28 +170,43 @@ export const getFormattedTablesByLayers = ( formatFactory: FormatFactory ): DatatablesWithFormatInfo => layers.reduce( - (formattedDatatables, { layerId, table, xAccessor, xScaleType }) => ({ + (formattedDatatables, { layerId, table, xAccessor, splitAccessor, accessors, xScaleType }) => ({ ...formattedDatatables, - [layerId]: getFormattedTable(table, formatFactory, xAccessor, xScaleType), + [layerId]: getFormattedTable( + table, + formatFactory, + xAccessor, + [xAccessor, splitAccessor, ...accessors].filter( + (a): a is string | ExpressionValueVisDimension => a !== undefined + ), + xScaleType + ), }), {} ); const getSeriesName: GetSeriesNameFn = ( data, - { layer, splitHint, splitFormatter, alreadyFormattedColumns, columnToLabelMap } + { + splitColumnId, + accessorsCount, + splitHint, + splitFormatter, + alreadyFormattedColumns, + columnToLabelMap, + } ) => { // For multiple y series, the name of the operation is used on each, either: // * Key - Y name // * Formatted value - Y name - if (layer.splitAccessor && layer.accessors.length > 1) { - const formatted = alreadyFormattedColumns[layer.splitAccessor]; + if (splitColumnId && accessorsCount > 1) { + const formatted = alreadyFormattedColumns[splitColumnId]; const result = data.seriesKeys .map((key: string | number, i) => { - if (i === 0 && splitHint && layer.splitAccessor && !formatted) { + if (i === 0 && splitHint && splitColumnId && !formatted) { return splitFormatter.convert(key); } - return layer.splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? null; + return splitColumnId && i === 0 ? key : columnToLabelMap[key] ?? null; }) .join(' - '); return result; @@ -184,7 +215,7 @@ const getSeriesName: GetSeriesNameFn = ( // For formatted split series, format the key // This handles splitting by dates, for example if (splitHint) { - if (layer.splitAccessor && alreadyFormattedColumns[layer.splitAccessor]) { + if (splitColumnId && alreadyFormattedColumns[splitColumnId]) { return data.seriesKeys[0]; } return splitFormatter.convert(data.seriesKeys[0]); @@ -192,7 +223,7 @@ const getSeriesName: GetSeriesNameFn = ( // This handles both split and single-y cases: // * If split series without formatting, show the value literally // * If single Y, the seriesKey will be the accessor, so we show the human-readable name - return layer.splitAccessor ? data.seriesKeys[0] : columnToLabelMap[data.seriesKeys[0]] ?? null; + return splitColumnId ? data.seriesKeys[0] : columnToLabelMap[data.seriesKeys[0]] ?? null; }; const getPointConfig = (xAccessor?: string, emphasizeFitting?: boolean) => ({ @@ -249,13 +280,18 @@ export const getSeriesProps: GetSeriesPropsFn = ({ const isStacked = layer.seriesType.includes('stacked'); const isPercentage = layer.seriesType.includes('percentage'); const isBarChart = layer.seriesType.includes('bar'); + const xColumnId = layer.xAccessor && getAccessorByDimension(layer.xAccessor, table.columns); + const splitColumnId = + layer.splitAccessor && getAccessorByDimension(layer.splitAccessor, table.columns); const enableHistogramMode = layer.isHistogram && (isStacked || !layer.splitAccessor) && (isStacked || !isBarChart || !chartHasMoreThanOneBarSeries); const formatter = table?.columns.find((column) => column.id === accessor)?.meta?.params; - const splitHint = table?.columns.find((col) => col.id === layer.splitAccessor)?.meta?.params; + const splitHint = layer.splitAccessor + ? getFormatByAccessor(layer.splitAccessor, table.columns) + : undefined; const splitFormatter = formatFactory(splitHint); // what if row values are not primitive? That is the case of, for instance, Ranges @@ -267,15 +303,15 @@ export const getSeriesProps: GetSeriesPropsFn = ({ // To not display them in the legend, they need to be filtered out. let rows = formattedTable.rows.filter( (row) => - !(layer.xAccessor && typeof row[layer.xAccessor] === 'undefined') && + !(xColumnId && typeof row[xColumnId] === 'undefined') && !( - layer.splitAccessor && - typeof row[layer.splitAccessor] === 'undefined' && + splitColumnId && + typeof row[splitColumnId] === 'undefined' && typeof row[accessor] === 'undefined' ) ); - if (!layer.xAccessor) { + if (!xColumnId) { rows = rows.map((row) => ({ ...row, unifiedX: i18n.translate('expressionXY.xyChart.emptyXLabel', { @@ -285,17 +321,17 @@ export const getSeriesProps: GetSeriesPropsFn = ({ } return { - splitSeriesAccessors: layer.splitAccessor ? [layer.splitAccessor] : [], - stackAccessors: isStacked ? [layer.xAccessor as string] : [], - id: layer.splitAccessor ? `${layer.splitAccessor}-${accessor}` : `${accessor}`, - xAccessor: layer.xAccessor || 'unifiedX', + splitSeriesAccessors: splitColumnId ? [splitColumnId] : [], + stackAccessors: isStacked ? [xColumnId as string] : [], + id: splitColumnId ? `${splitColumnId}-${accessor}` : accessor, + xAccessor: xColumnId || 'unifiedX', yAccessors: [accessor], data: rows, - xScaleType: layer.xAccessor ? layer.xScaleType : 'ordinal', + xScaleType: xColumnId ? layer.xScaleType : 'ordinal', yScaleType: - formatter?.id === 'bytes' && layer.yScaleType === ScaleType.Linear + formatter?.id === 'bytes' && yAxis?.scale === ScaleType.Linear ? ScaleType.LinearBinary - : layer.yScaleType, + : yAxis?.scale || ScaleType.Linear, color: (series) => getColor(series, { layer, @@ -310,19 +346,20 @@ export const getSeriesProps: GetSeriesPropsFn = ({ stackMode: isPercentage ? StackMode.Percentage : undefined, timeZone, areaSeriesStyle: { - point: getPointConfig(layer.xAccessor, emphasizeFitting), + point: getPointConfig(xColumnId, emphasizeFitting), ...(fillOpacity && { area: { opacity: fillOpacity } }), ...(emphasizeFitting && { fit: { area: { opacity: fillOpacity || 0.5 }, line: getLineConfig() }, }), }, lineSeriesStyle: { - point: getPointConfig(layer.xAccessor, emphasizeFitting), + point: getPointConfig(xColumnId, emphasizeFitting), ...(emphasizeFitting && { fit: { line: getLineConfig() } }), }, name(d) { return getSeriesName(d, { - layer, + splitColumnId, + accessorsCount: layer.accessors.length, splitHint, splitFormatter, alreadyFormattedColumns: formattedColumns, diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/format.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/format.ts index 480aea5d2552b..3830f9cadead6 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/format.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/format.ts @@ -6,6 +6,22 @@ * Side Public License, v 1. */ -import { DatatableColumnMeta } from '@kbn/expressions-plugin'; +import { DatatableColumn } from '@kbn/expressions-plugin'; +import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; +import { getFormatByAccessor, getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; -export const getFormat = (meta?: DatatableColumnMeta) => meta?.params || { id: meta?.type }; +export const getFormat = ( + columns: DatatableColumn[], + accessor: string | ExpressionValueVisDimension +) => { + const type = getColumnByAccessor(accessor, columns)?.meta.type; + return getFormatByAccessor( + accessor, + columns, + type + ? { + id: type, + } + : undefined + ); +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts index 17e7a9c2aba32..015cab5431e9e 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts @@ -7,6 +7,7 @@ */ import { search } from '@kbn/data-plugin/public'; +import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { XYChartProps } from '../../common'; import { getFilteredLayers } from './layers'; import { isDataLayer } from './visualization'; @@ -15,9 +16,10 @@ export function calculateMinInterval({ args: { layers } }: XYChartProps) { const filteredLayers = getFilteredLayers(layers); if (filteredLayers.length === 0) return; const isTimeViz = filteredLayers.every((l) => isDataLayer(l) && l.xScaleType === 'time'); - const xColumn = filteredLayers[0].table.columns.find( - (column) => isDataLayer(filteredLayers[0]) && column.id === filteredLayers[0].xAccessor - ); + const xColumn = + isDataLayer(filteredLayers[0]) && + filteredLayers[0].xAccessor && + getColumnByAccessor(filteredLayers[0].xAccessor, filteredLayers[0].table.columns); if (!xColumn) return; if (!isTimeViz) { diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts index 4408ebd3feb84..9934cc4f78fa9 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts @@ -7,6 +7,8 @@ */ import { Datatable } from '@kbn/expressions-plugin/common'; +import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; +import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { CommonXYDataLayerConfig, CommonXYLayerConfig, @@ -18,20 +20,24 @@ export function getFilteredLayers(layers: CommonXYLayerConfig[]) { return layers.filter( (layer): layer is CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig => { let table: Datatable | undefined; - let accessors: string[] = []; + let accessors: Array = []; let xAccessor: undefined | string | number; let splitAccessor: undefined | string | number; - if (isDataLayer(layer)) { - xAccessor = layer.xAccessor; - splitAccessor = layer.splitAccessor; - } - if (isDataLayer(layer) || isReferenceLayer(layer)) { table = layer.table; accessors = layer.accessors; } + if (isDataLayer(layer)) { + xAccessor = + layer.xAccessor && table && getAccessorByDimension(layer.xAccessor, table.columns); + splitAccessor = + layer.splitAccessor && + table && + getAccessorByDimension(layer.splitAccessor, table.columns); + } + return !( !accessors.length || !table || 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/edit_index_pattern/tabs/tabs.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx index a50e1062f447f..35323d90a7efb 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -448,7 +448,7 @@ export function Tabs({ getFieldInfo, }} openModal={overlays.openModal} - theme={theme!} + theme={theme} userEditPermission={dataViews.getCanSaveSync()} /> )} 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/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx index 0ef1c47541302..ecff28546e409 100644 --- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx @@ -67,6 +67,7 @@ export async function mountManagementSection( IndexPatternEditor: dataViewEditor.IndexPatternEditorComponent, fieldFormats, spaces, + theme, }; ReactDOM.render( diff --git a/src/plugins/data_view_management/public/mocks.ts b/src/plugins/data_view_management/public/mocks.ts index f86a4cc724704..d1e5eac4b2be0 100644 --- a/src/plugins/data_view_management/public/mocks.ts +++ b/src/plugins/data_view_management/public/mocks.ts @@ -56,7 +56,8 @@ const docLinks = { const createIndexPatternManagmentContext = (): { [key in keyof IndexPatternManagmentContext]: any; } => { - const { application, chrome, uiSettings, notifications, overlays } = coreMock.createStart(); + const { application, chrome, uiSettings, notifications, overlays, theme } = + coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); const dataViewFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); @@ -81,6 +82,7 @@ const createIndexPatternManagmentContext = (): { IndexPatternEditor: indexPatternEditorPluginMock.createStartContract().IndexPatternEditorComponent, fieldFormats: fieldFormatsServiceMock.createStartContract(), + theme, }; }; diff --git a/src/plugins/data_view_management/public/types.ts b/src/plugins/data_view_management/public/types.ts index 56e9735a9001f..0901ba72d050b 100644 --- a/src/plugins/data_view_management/public/types.ts +++ b/src/plugins/data_view_management/public/types.ts @@ -14,6 +14,7 @@ import { DocLinksStart, HttpSetup, ApplicationStart, + ThemeServiceStart, } from '@kbn/core/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; @@ -44,6 +45,7 @@ export interface IndexPatternManagmentContext { IndexPatternEditor: DataViewEditorStart['IndexPatternEditorComponent']; fieldFormats: FieldFormatsStart; spaces?: SpacesPluginStart; + theme: ThemeServiceStart; } export type IndexPatternManagmentContextValue = 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/inspector/public/views/requests/index.ts b/src/plugins/inspector/public/views/requests/index.ts index ce1cb766a2985..37d446958e8b2 100644 --- a/src/plugins/inspector/public/views/requests/index.ts +++ b/src/plugins/inspector/public/views/requests/index.ts @@ -20,7 +20,7 @@ export const getRequestsViewDescription = (): InspectorViewDescription => ({ }), order: 20, help: i18n.translate('inspector.requests.requestsDescriptionTooltip', { - defaultMessage: 'View the requests that collected the data', + defaultMessage: 'View the search requests used to collect the data', }), shouldShow(adapters: Adapters) { return Boolean(adapters.requests); 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/telemetry/public/plugin.test.ts b/src/plugins/telemetry/public/plugin.test.ts index f25bf92340ba6..13ebb4d999315 100644 --- a/src/plugins/telemetry/public/plugin.test.ts +++ b/src/plugins/telemetry/public/plugin.test.ts @@ -66,7 +66,19 @@ describe('TelemetryPlugin', () => { expect(coreSetupMock.analytics.registerShipper).toHaveBeenCalledWith( ElasticV3BrowserShipper, - { channelName: 'kibana-browser', version: 'version' } + { channelName: 'kibana-browser', version: 'version', sendTo: 'staging' } + ); + }); + + it('registers the UI telemetry shipper (pointing to prod)', () => { + const initializerContext = coreMock.createPluginInitializerContext({ sendUsageTo: 'prod' }); + const coreSetupMock = coreMock.createSetup(); + + new TelemetryPlugin(initializerContext).setup(coreSetupMock, { screenshotMode, home }); + + expect(coreSetupMock.analytics.registerShipper).toHaveBeenCalledWith( + ElasticV3BrowserShipper, + { channelName: 'kibana-browser', version: 'version', sendTo: 'production' } ); }); }); diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index 6fe7fe4138a31..dea9cd1df2c3c 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -158,6 +158,7 @@ export class TelemetryPlugin implements Plugin { diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 0c0cafad6bec6..adabf8ea3d854 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -9610,6 +9610,142 @@ } } }, + "usage_collector_stats": { + "properties": { + "total_duration": { + "type": "long", + "_meta": { + "description": "The total execution duration to grab usage stats for all collectors in milliseconds" + } + }, + "total_is_ready_duration": { + "type": "long", + "_meta": { + "description": "The total execution duration of the isReady function for all collectors in milliseconds" + } + }, + "total_fetch_duration": { + "type": "long", + "_meta": { + "description": "The total execution duration of the fetch function for all ready collectors in milliseconds" + } + }, + "is_ready_duration_breakdown": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "keyword", + "_meta": { + "description": "The name of the collector" + } + }, + "duration": { + "type": "long", + "_meta": { + "description": "The execution duration of the isReady function for the collector in milliseconds" + } + } + } + } + }, + "fetch_duration_breakdown": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "keyword", + "_meta": { + "description": "The name of the collector" + } + }, + "duration": { + "type": "long", + "_meta": { + "description": "The execution duration of the fetch function for the collector in milliseconds" + } + } + } + } + }, + "not_ready": { + "properties": { + "count": { + "type": "short", + "_meta": { + "description": "The number of collectors that returned false from the isReady function" + } + }, + "names": { + "type": "array", + "items": { + "type": "keyword", + "_meta": { + "description": "The name of the of collectors that returned false from the isReady function" + } + } + } + } + }, + "not_ready_timeout": { + "properties": { + "count": { + "type": "short", + "_meta": { + "description": "The number of collectors that timedout during the isReady function" + } + }, + "names": { + "type": "array", + "items": { + "type": "keyword", + "_meta": { + "description": "The name of collectors that timedout during the isReady function" + } + } + } + } + }, + "succeeded": { + "properties": { + "count": { + "type": "short", + "_meta": { + "description": "The number of collectors that returned true from the fetch function" + } + }, + "names": { + "type": "array", + "items": { + "type": "keyword", + "_meta": { + "description": "The name of the of collectors that returned true from the fetch function" + } + } + } + } + }, + "failed": { + "properties": { + "count": { + "type": "short", + "_meta": { + "description": "The number of collectors that threw an error from the fetch function" + } + }, + "names": { + "type": "array", + "items": { + "type": "keyword", + "_meta": { + "description": "The name of the of collectors that threw an error from the fetch function" + } + } + } + } + } + } + }, "vis_type_table": { "properties": { "total": { diff --git a/src/plugins/telemetry/schema/oss_root.json b/src/plugins/telemetry/schema/oss_root.json index cf9b881facef2..e526dc6413916 100644 --- a/src/plugins/telemetry/schema/oss_root.json +++ b/src/plugins/telemetry/schema/oss_root.json @@ -194,62 +194,6 @@ "properties": { "kibana_config_usage": { "type": "pass_through" - }, - "usage_collector_stats": { - "properties": { - "not_ready": { - "properties": { - "count": { - "type": "short" - }, - "names": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "not_ready_timeout": { - "properties": { - "count": { - "type": "short" - }, - "names": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "succeeded": { - "properties": { - "count": { - "type": "short" - }, - "names": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - }, - "failed": { - "properties": { - "count": { - "type": "short" - }, - "names": { - "type": "array", - "items": { - "type": "keyword" - } - } - } - } - } } } } diff --git a/src/plugins/telemetry/server/plugin.test.ts b/src/plugins/telemetry/server/plugin.test.ts index c31e78722abf5..9f7d0cfcf812f 100644 --- a/src/plugins/telemetry/server/plugin.test.ts +++ b/src/plugins/telemetry/server/plugin.test.ts @@ -26,7 +26,22 @@ describe('TelemetryPlugin', () => { expect(coreSetupMock.analytics.registerShipper).toHaveBeenCalledWith( ElasticV3ServerShipper, - { channelName: 'kibana-server', version: 'version' } + { channelName: 'kibana-server', version: 'version', sendTo: 'staging' } + ); + }); + + it('registers the Server telemetry shipper (sendTo: production)', () => { + const initializerContext = coreMock.createPluginInitializerContext({ sendUsageTo: 'prod' }); + const coreSetupMock = coreMock.createSetup(); + + new TelemetryPlugin(initializerContext).setup(coreSetupMock, { + usageCollection: usageCollectionPluginMock.createSetupContract(), + telemetryCollectionManager: telemetryCollectionManagerPluginMock.createSetupContract(), + }); + + expect(coreSetupMock.analytics.registerShipper).toHaveBeenCalledWith( + ElasticV3ServerShipper, + { channelName: 'kibana-server', version: 'version', sendTo: 'production' } ); }); }); diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index c3c2604dc9a03..881407fb8e288 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -85,6 +85,7 @@ type SavedObjectsRegisterType = CoreSetup['savedObjects']['registerType']; export class TelemetryPlugin implements Plugin { private readonly logger: Logger; private readonly currentKibanaVersion: string; + private readonly initialConfig: TelemetryConfigType; private readonly config$: Observable; private readonly isOptedIn$ = new BehaviorSubject(undefined); private readonly isDev: boolean; @@ -122,13 +123,14 @@ export class TelemetryPlugin implements Plugin
    `; 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 [
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    ,
    { + // WORKAROUND: wrong Axios types, should be fixed in https://github.com/axios/axios/pull/4475 + const getDefaultHeader = (axiosInstance: AxiosInstance, headerName: string) => + (axiosInstance.defaults.headers as HeadersDefaults & Record)[headerName]; + it('test fetch headers', () => { - expect(fetch.defaults.headers.Accept).toBe('application/json'); - expect(fetch.defaults.headers['Content-Type']).toBe('application/json'); - expect(fetch.defaults.headers['kbn-xsrf']).toBe('professionally-crafted-string-of-text'); + expect(getDefaultHeader(fetch, 'Accept')).toBe('application/json'); + expect(getDefaultHeader(fetch, 'Content-Type')).toBe('application/json'); + expect(getDefaultHeader(fetch, 'kbn-xsrf')).toBe('professionally-crafted-string-of-text'); }); it('test arrayBufferFetch headers', () => { - expect(arrayBufferFetch.defaults.headers.Accept).toBe('application/json'); - expect(arrayBufferFetch.defaults.headers['Content-Type']).toBe('application/json'); - expect(arrayBufferFetch.defaults.headers['kbn-xsrf']).toBe( + expect(getDefaultHeader(arrayBufferFetch, 'Accept')).toBe('application/json'); + expect(getDefaultHeader(arrayBufferFetch, 'Content-Type')).toBe('application/json'); + expect(getDefaultHeader(arrayBufferFetch, 'kbn-xsrf')).toBe( 'professionally-crafted-string-of-text' ); expect(arrayBufferFetch.defaults.responseType).toBe('arraybuffer'); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot index 587b07ca4f932..ce45f123172dc 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot @@ -29,7 +29,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = `





    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`] = `




    => Promise.resolve(casesStatus); + +export const getCasesMetrics = async ({ + http, + signal, + query, +}: HTTPService & { query: CasesMetricsRequest }): Promise => + Promise.resolve(casesMetrics); diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index dbc57e163d3ff..908a0dd5d52df 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -148,7 +148,7 @@ export const TAGS_HELP = i18n.translate('xpack.cases.createCase.fieldTagsHelpTex }); export const TAGS_EMPTY_ERROR = i18n.translate('xpack.cases.createCase.fieldTagsEmptyError', { - defaultMessage: 'A tag must contain at least one non-space character', + defaultMessage: 'A tag must contain at least one non-space character.', }); export const NO_TAGS = i18n.translate('xpack.cases.caseView.noTags', { @@ -229,8 +229,7 @@ export const SYNC_ALERTS_SWITCH_LABEL_OFF = i18n.translate( ); export const SYNC_ALERTS_HELP = i18n.translate('xpack.cases.components.create.syncAlertHelpText', { - defaultMessage: - 'Enabling this option will sync the status of alerts in this case with the case status.', + defaultMessage: 'Enabling this option will sync the alert statuses with the case status.', }); export const ALERT = i18n.translate('xpack.cases.common.alertLabel', { @@ -268,18 +267,18 @@ export const CASE_SUCCESS_TOAST = (title: string) => 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/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 22e12d5ee11b5..853a32eaabbaf 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -36,12 +36,14 @@ import { waitForComponentToUpdate } from '../../common/test_utils'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; +import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; jest.mock('../../containers/use_create_attachments'); jest.mock('../../containers/use_bulk_update_case'); jest.mock('../../containers/use_delete_cases'); jest.mock('../../containers/use_get_cases'); jest.mock('../../containers/use_get_cases_status'); +jest.mock('../../containers/use_get_cases_metrics'); jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_get_reporters'); @@ -55,6 +57,7 @@ jest.mock('../app/use_available_owners', () => ({ const useDeleteCasesMock = useDeleteCases as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; +const useGetCasesMetricsMock = useGetCasesMetrics as jest.Mock; const useUpdateCasesMock = useUpdateCases as jest.Mock; const useGetActionLicenseMock = useGetActionLicense as jest.Mock; const useGetTagsMock = useGetTags as jest.Mock; @@ -118,6 +121,12 @@ describe('AllCasesListGeneric', () => { isLoading: false, }; + const defaultCasesMetrics = { + mttr: 5, + isLoading: false, + fetchCasesMetrics: jest.fn(), + }; + const defaultUpdateCases = { isUpdated: false, isLoading: false, @@ -157,6 +166,7 @@ describe('AllCasesListGeneric', () => { useGetCasesMock.mockReturnValue(defaultGetCases); useDeleteCasesMock.mockReturnValue(defaultDeleteCases); useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); + useGetCasesMetricsMock.mockReturnValue(defaultCasesMetrics); useGetActionLicenseMock.mockReturnValue(defaultActionLicense); useGetTagsMock.mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags: jest.fn() }); useGetReportersMock.mockReturnValue({ @@ -215,6 +225,24 @@ describe('AllCasesListGeneric', () => { }); }); + it('should show a tooltip with all tags when hovered', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open }, + }); + const result = render( + + + + ); + + userEvent.hover(result.queryAllByTestId('case-table-column-tags')[0]); + + await waitFor(() => { + expect(result.getByTestId('case-table-column-tags-tooltip')).toBeTruthy(); + }); + }); + it('should render empty fields', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, @@ -324,6 +352,15 @@ describe('AllCasesListGeneric', () => { }); }); + it('should render the case stats', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="cases-count-stats"]')).toBeTruthy(); + }); + it.skip('Bulk delete', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 96b220283b452..86933b1395b38 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -27,6 +27,7 @@ import { EuiBasicTableOnChange } from './types'; import { CasesTable } from './table'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useCasesContext } from '../cases_context/use_cases_context'; +import { CasesMetrics } from './cases_metrics'; const ProgressLoader = styled(EuiProgress)` ${({ $isShow }: { $isShow: boolean }) => @@ -56,6 +57,7 @@ export const AllCasesList = React.memo( const { owner, userCanCrud } = useCasesContext(); const hasOwner = !!owner.length; const availableSolutions = useAvailableCasesOwners(); + const [refresh, setRefresh] = useState(0); const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses)); const initialFilterOptions = { @@ -104,8 +106,13 @@ export const AllCasesList = React.memo( const refreshCases = useCallback( (dataRefresh = true) => { deselectCases(); - if (dataRefresh) refetchCases(); - if (doRefresh) doRefresh(); + if (dataRefresh) { + refetchCases(); + setRefresh((currRefresh: number) => currRefresh + 1); + } + if (doRefresh) { + doRefresh(); + } if (filterRefetch.current != null) { filterRefetch.current(); } @@ -206,6 +213,7 @@ export const AllCasesList = React.memo( className="essentialAnimation" $isShow={(isCasesLoading || isLoading) && !isDataEmpty} /> + { + useGetCasesStatusMock.mockReturnValue({ + countOpenCases: 2, + countInProgressCases: 3, + countClosedCases: 4, + isLoading: false, + fetchCasesStatus: jest.fn(), + }); + useGetCasesMetricsMock.mockReturnValue({ + // 600 seconds = 10m + mttr: 600, + isLoading: false, + fetchCasesMetrics: jest.fn(), + }); + + let appMockRenderer: AppMockRenderer; + + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + }); + + it('renders the correct stats', () => { + const result = appMockRenderer.render(); + expect(result.getByTestId('cases-metrics-stats')).toBeTruthy(); + expect(within(result.getByTestId('openStatsHeader')).getByText(2)).toBeTruthy(); + expect(within(result.getByTestId('inProgressStatsHeader')).getByText(3)).toBeTruthy(); + expect(within(result.getByTestId('closedStatsHeader')).getByText(4)).toBeTruthy(); + expect(within(result.getByTestId('mttrStatsHeader')).getByText('10m')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx new file mode 100644 index 0000000000000..3325b6de4ebcc --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useEffect, useMemo } from 'react'; +import { + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingSpinner, + EuiToolTip, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import prettyMilliseconds from 'pretty-ms'; +import { CaseStatuses } from '../../../common/api'; +import { useGetCasesStatus } from '../../containers/use_get_cases_status'; +import { StatusStats } from '../status/status_stats'; +import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; +import { ATTC_DESCRIPTION, ATTC_STAT } from './translations'; + +interface CountProps { + refresh?: number; +} +const MetricsFlexGroup = styled.div` + ${({ theme }) => css` + .euiFlexGroup { + border: ${theme.eui.euiBorderThin}; + border-radius: ${theme.eui.euiBorderRadius}; + margin: 0 0 ${theme.eui.euiSizeL} 0; + } + @media only screen and (max-width: ${theme.eui.euiBreakpoints.s}) { + .euiFlexGroup { + padding: ${theme.eui.euiSizeM}; + } + } + `} +`; + +export const CasesMetrics: FunctionComponent = ({ refresh }) => { + const { + countOpenCases, + countInProgressCases, + countClosedCases, + isLoading: isCasesStatusLoading, + fetchCasesStatus, + } = useGetCasesStatus(); + + const { mttr, isLoading: isCasesMetricsLoading, fetchCasesMetrics } = useGetCasesMetrics(); + + const mttrValue = useMemo( + () => (mttr ? prettyMilliseconds(mttr * 1000, { compact: true, verbose: false }) : '-'), + [mttr] + ); + + useEffect(() => { + if (refresh != null) { + fetchCasesStatus(); + fetchCasesMetrics(); + } + }, [fetchCasesMetrics, fetchCasesStatus, refresh]); + + return ( + + + + + + + + + + + + + + <> + {ATTC_STAT} + + + ), + description: isCasesMetricsLoading ? ( + + ) : ( + mttrValue + ), + }, + ]} + /> + + + + ); +}; +CasesMetrics.displayName = 'CasesMetrics'; diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 43096d3de061c..c895dfdc11f3f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -19,6 +19,7 @@ import { EuiFlexItem, EuiIcon, EuiHealth, + EuiToolTip, } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; @@ -56,10 +57,6 @@ const Spacer = styled.span` margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const TagWrapper = styled(EuiBadgeGroup)` - width: 100%; -`; - const renderStringField = (field: string, dataTestSubj: string) => field != null ? {field} : getEmptyTagValue(); @@ -205,8 +202,8 @@ export const useCasesColumns = ({ name: i18n.TAGS, render: (tags: Case['tags']) => { if (tags != null && tags.length > 0) { - return ( - + const badges = ( + {tags.map((tag: string, i: number) => ( ))} - + + ); + + return ( + + {badges} + ); } return getEmptyTagValue(); diff --git a/x-pack/plugins/cases/public/components/all_cases/count.tsx b/x-pack/plugins/cases/public/components/all_cases/count.tsx deleted file mode 100644 index cd4abdd08b8e7..0000000000000 --- a/x-pack/plugins/cases/public/components/all_cases/count.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { CaseStatuses } from '../../../common/api'; -import { Stats } from '../status'; -import { useGetCasesStatus } from '../../containers/use_get_cases_status'; - -interface CountProps { - refresh?: number; -} -export const Count: FunctionComponent = ({ refresh }) => { - const { - countOpenCases, - countInProgressCases, - countClosedCases, - isLoading: isCasesStatusLoading, - fetchCasesStatus, - } = useGetCasesStatus(); - useEffect(() => { - if (refresh != null) { - fetchCasesStatus(); - } - }, [fetchCasesStatus, refresh]); - return ( - - - - - - - - - - - - ); -}; -Count.displayName = 'Count'; diff --git a/x-pack/plugins/cases/public/components/all_cases/header.tsx b/x-pack/plugins/cases/public/components/all_cases/header.tsx index 4e66083711e2b..9a02594a790fa 100644 --- a/x-pack/plugins/cases/public/components/all_cases/header.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/header.tsx @@ -7,60 +7,26 @@ import React, { FunctionComponent } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled, { css } from 'styled-components'; import { HeaderPage } from '../header_page'; import * as i18n from './translations'; -import { Count } from './count'; import { ErrorMessage } from '../use_push_to_service/callout/types'; import { NavButtons } from './nav_buttons'; interface OwnProps { actionsErrors: ErrorMessage[]; - refresh: number; userCanCrud: boolean; } type Props = OwnProps; -const FlexItemDivider = styled(EuiFlexItem)` - ${({ theme }) => css` - .euiFlexGroup--gutterMedium > &.euiFlexItem { - border-right: ${theme.eui.euiBorderThin}; - padding-right: ${theme.eui.euiSize}; - margin-right: ${theme.eui.euiSize}; - @media only screen and (max-width: ${theme.eui.euiBreakpoints.l}) { - padding-right: 0; - border-right: 0; - margin-right: 0; - } - } - `} -`; - -export const CasesTableHeader: FunctionComponent = ({ - actionsErrors, - refresh, - userCanCrud, -}) => ( +export const CasesTableHeader: FunctionComponent = ({ actionsErrors, userCanCrud }) => ( {userCanCrud ? ( - <> - - - - - - - - - ) : ( - // doesn't include the horizontal bar that divides the buttons and other padding since we don't have any buttons - // to the right - + - )} + ) : null} ); diff --git a/x-pack/plugins/cases/public/components/all_cases/index.tsx b/x-pack/plugins/cases/public/components/all_cases/index.tsx index 8ea7681eb44d9..c2811df9a684d 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { CasesDeepLinkId } from '../../common/navigation'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { useCasesContext } from '../cases_context/use_cases_context'; @@ -16,20 +16,15 @@ import { CasesTableHeader } from './header'; export const AllCases: React.FC = () => { const { userCanCrud } = useCasesContext(); - const [refresh, setRefresh] = useState(0); useCasesBreadcrumbs(CasesDeepLinkId.cases); - const doRefresh = useCallback(() => { - setRefresh((prev) => prev + 1); - }, [setRefresh]); - const { actionLicense } = useGetActionLicense(); const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); return ( <> - - + + ); }; diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index 4b6a86a2592e6..e56ac8b0da655 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -105,3 +105,12 @@ export const STATUS = i18n.translate('xpack.cases.caseTable.status', { export const CHANGE_STATUS = i18n.translate('xpack.cases.caseTable.changeStatus', { defaultMessage: 'Change status', }); + +export const ATTC_STAT = i18n.translate('xpack.cases.casesStats.mttr', { + defaultMessage: 'Average time to close', +}); + +export const ATTC_DESCRIPTION = i18n.translate('xpack.cases.casesStats.mttrDescription', { + defaultMessage: + 'Average time to close is the average duration of cases from creation to closure.', +}); 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/cases/public/components/status/index.ts b/x-pack/plugins/cases/public/components/status/index.ts index 94d7cb6a31830..a261b903ae9ce 100644 --- a/x-pack/plugins/cases/public/components/status/index.ts +++ b/x-pack/plugins/cases/public/components/status/index.ts @@ -7,5 +7,5 @@ export * from './status'; export * from './config'; -export * from './stats'; +export * from './status_stats'; export * from './types'; diff --git a/x-pack/plugins/cases/public/components/status/stats.test.tsx b/x-pack/plugins/cases/public/components/status/status_stats.test.tsx similarity index 75% rename from x-pack/plugins/cases/public/components/status/stats.test.tsx rename to x-pack/plugins/cases/public/components/status/status_stats.test.tsx index ea0f54bf8055b..28292496dd917 100644 --- a/x-pack/plugins/cases/public/components/status/stats.test.tsx +++ b/x-pack/plugins/cases/public/components/status/status_stats.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { CaseStatuses } from '../../../common/api'; -import { Stats } from './stats'; +import { StatusStats } from './status_stats'; describe('Stats', () => { const defaultProps = { @@ -19,13 +19,13 @@ describe('Stats', () => { dataTestSubj: 'test-stats', }; it('it renders', async () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find(`[data-test-subj="test-stats"]`).exists()).toBeTruthy(); }); it('shows the count', async () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__description`).first().text() @@ -33,14 +33,14 @@ describe('Stats', () => { }); it('shows the loading spinner', async () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find(`[data-test-subj="test-stats-loading-spinner"]`).exists()).toBeTruthy(); }); describe('Status title', () => { it('shows the correct title for status open', async () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text() @@ -48,7 +48,9 @@ describe('Stats', () => { }); it('shows the correct title for status in-progress', async () => { - const wrapper = mount(); + const wrapper = mount( + + ); expect( wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text() @@ -56,7 +58,7 @@ describe('Stats', () => { }); it('shows the correct title for status closed', async () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text() diff --git a/x-pack/plugins/cases/public/components/status/stats.tsx b/x-pack/plugins/cases/public/components/status/status_stats.tsx similarity index 80% rename from x-pack/plugins/cases/public/components/status/stats.tsx rename to x-pack/plugins/cases/public/components/status/status_stats.tsx index 98720ad75a656..56f4259f87ea6 100644 --- a/x-pack/plugins/cases/public/components/status/stats.tsx +++ b/x-pack/plugins/cases/public/components/status/status_stats.tsx @@ -17,7 +17,12 @@ export interface Props { dataTestSubj?: string; } -const StatsComponent: React.FC = ({ caseCount, caseStatus, isLoading, dataTestSubj }) => { +const StatusStatsComponent: React.FC = ({ + caseCount, + caseStatus, + isLoading, + dataTestSubj, +}) => { const statusStats = useMemo( () => [ { @@ -25,7 +30,7 @@ const StatsComponent: React.FC = ({ caseCount, caseStatus, isLoading, dat description: isLoading ? ( ) : ( - caseCount ?? 'N/A' + caseCount ?? '-' ), }, ], @@ -36,5 +41,5 @@ const StatsComponent: React.FC = ({ caseCount, caseStatus, isLoading, dat ); }; -StatsComponent.displayName = 'StatsComponent'; -export const Stats = memo(StatsComponent); +StatusStatsComponent.displayName = 'StatusStats'; +export const StatusStats = memo(StatusStatsComponent); diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index ed9e9ebd1ff8f..8cf413d08f2fd 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -12,6 +12,7 @@ import type { SingleCaseMetrics, SingleCaseMetricsFeature, AlertComment, + CasesMetrics, } from '../../common/ui/types'; import { Actions, @@ -292,6 +293,10 @@ export const casesStatus: CasesStatus = { countClosedCases: 130, }; +export const casesMetrics: CasesMetrics = { + mttr: 12, +}; + export const basicPush = { connectorId: '123', connectorName: 'connector name', diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx new file mode 100644 index 0000000000000..6601a104d9f7d --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import * as api from '../api'; +import { TestProviders } from '../common/mock'; +import { useGetCasesMetrics, UseGetCasesMetrics } from './use_get_cases_metrics'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; + +jest.mock('../api'); +jest.mock('../common/lib/kibana'); + +describe('useGetReporters', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + const { result } = renderHook(() => useGetCasesMetrics(), { + wrapper: ({ children }) => {children}, + }); + + await act(async () => { + expect(result.current).toEqual({ + mttr: 0, + isLoading: true, + isError: false, + fetchCasesMetrics: result.current.fetchCasesMetrics, + }); + }); + }); + + it('calls getCasesMetrics api', async () => { + const spy = jest.spyOn(api, 'getCasesMetrics'); + await act(async () => { + const { waitForNextUpdate } = renderHook( + () => useGetCasesMetrics(), + { + wrapper: ({ children }) => {children}, + } + ); + + await waitForNextUpdate(); + expect(spy).toBeCalledWith({ + http: expect.anything(), + signal: expect.anything(), + query: { + features: ['mttr'], + owner: [SECURITY_SOLUTION_OWNER], + }, + }); + }); + }); + + it('fetch cases metrics', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useGetCasesMetrics(), + { + wrapper: ({ children }) => {children}, + } + ); + + await waitForNextUpdate(); + expect(result.current).toEqual({ + mttr: 12, + isLoading: false, + isError: false, + fetchCasesMetrics: result.current.fetchCasesMetrics, + }); + }); + }); + + it('fetches metrics when fetchCasesMetrics is invoked', async () => { + const spy = jest.spyOn(api, 'getCasesMetrics'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useGetCasesMetrics(), + { + wrapper: ({ children }) => {children}, + } + ); + + await waitForNextUpdate(); + expect(spy).toBeCalledWith({ + http: expect.anything(), + signal: expect.anything(), + query: { + features: ['mttr'], + owner: [SECURITY_SOLUTION_OWNER], + }, + }); + result.current.fetchCasesMetrics(); + await waitForNextUpdate(); + expect(spy).toHaveBeenCalledTimes(2); + }); + }); + + it('unhappy path', async () => { + const spy = jest.spyOn(api, 'getCasesMetrics'); + spy.mockImplementation(() => { + throw new Error('Oh on. this is impossible'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useGetCasesMetrics(), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + mttr: 0, + isLoading: false, + isError: true, + fetchCasesMetrics: result.current.fetchCasesMetrics, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx new file mode 100644 index 0000000000000..a5cb116acc559 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState, useRef } from 'react'; + +import { useCasesContext } from '../components/cases_context/use_cases_context'; +import * as i18n from './translations'; +import { useHttp, useToasts } from '../common/lib/kibana'; +import { getCasesMetrics } from '../api'; +import { CasesMetrics } from './types'; + +interface CasesMetricsState extends CasesMetrics { + isLoading: boolean; + isError: boolean; +} + +const initialData: CasesMetricsState = { + mttr: 0, + isLoading: true, + isError: false, +}; + +export interface UseGetCasesMetrics extends CasesMetricsState { + fetchCasesMetrics: () => void; +} + +export const useGetCasesMetrics = (): UseGetCasesMetrics => { + const http = useHttp(); + const { owner } = useCasesContext(); + const [casesMetricsState, setCasesMetricsState] = useState(initialData); + const toasts = useToasts(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const fetchCasesMetrics = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + setCasesMetricsState({ + ...initialData, + isLoading: true, + }); + + const response = await getCasesMetrics({ + http, + signal: abortCtrlRef.current.signal, + query: { owner, features: ['mttr'] }, + }); + + if (!isCancelledRef.current) { + setCasesMetricsState({ + ...response, + isLoading: false, + isError: false, + }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + toasts.addError( + error.body && error.body.message ? new Error(error.body.message) : error, + { title: i18n.ERROR_TITLE } + ); + } + setCasesMetricsState({ + mttr: 0, + isLoading: false, + isError: true, + }); + } + } + }, [http, owner, toasts]); + + useEffect(() => { + fetchCasesMetrics(); + + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + }, [fetchCasesMetrics]); + + return { + ...casesMetricsState, + fetchCasesMetrics, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 30e9651b6e739..360827f7855f0 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -30,7 +30,6 @@ export const RULE_FAILED = `failed`; export const INTERNAL_FEATURE_FLAGS = { showBenchmarks: true, showManageRulesMock: false, - showRisksMock: false, showFindingsGroupBy: true, } as const; 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/public/common/hooks/use_url_query.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_url_query.ts index 2eea943f757cf..0573d77e6f9c8 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_url_query.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_url_query.ts @@ -33,7 +33,6 @@ export const useUrlQuery = (getDefaultQuery: () => T) => { // Set initial query useEffect(() => { - // TODO: condition should be if decoding failed if (search) return; replace({ search: encodeQuery(getDefaultQuery() as RisonObject) }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/risks_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/risks_table.tsx index b6b309cadbb02..16abf969f3062 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/risks_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/risks_table.tsx @@ -8,6 +8,7 @@ import React, { useMemo } from 'react'; import { EuiBasicTable, + EuiBasicTableColumn, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, @@ -17,46 +18,6 @@ import { import { ComplianceDashboardData, GroupedFindingsEvaluation } from '../../../../common/types'; import { CompactFormattedNumber } from '../../../components/compact_formatted_number'; import * as TEXT from '../translations'; -import { INTERNAL_FEATURE_FLAGS } from '../../../../common/constants'; - -const mockData = [ - { - name: 'pods', - totalFindings: 2, - totalPassed: 1, - totalFailed: 1, - }, - { - name: 'etcd', - totalFindings: 5, - totalPassed: 0, - totalFailed: 5, - }, - { - name: 'cluster', - totalFindings: 2, - totalPassed: 2, - totalFailed: 0, - }, - { - name: 'system', - totalFindings: 10, - totalPassed: 6, - totalFailed: 4, - }, - { - name: 'api', - totalFindings: 19100, - totalPassed: 2100, - totalFailed: 17000, - }, - { - name: 'server', - totalFindings: 7, - totalPassed: 4, - totalFailed: 3, - }, -]; export interface RisksTableProps { data: ComplianceDashboardData['groupedFindingsEvaluation']; @@ -81,13 +42,16 @@ export const RisksTable = ({ onCellClick, onViewAllClick, }: RisksTableProps) => { - const columns = useMemo( + const columns: Array> = useMemo( () => [ { field: 'name', + truncateText: true, name: TEXT.CIS_SECTION, render: (name: GroupedFindingsEvaluation['name']) => ( - onCellClick(name)}>{name} + onCellClick(name)} className="eui-textTruncate"> + {name} + ), }, { @@ -119,7 +83,7 @@ export const RisksTable = ({ rowHeader="name" - items={INTERNAL_FEATURE_FLAGS.showRisksMock ? getTopRisks(mockData, maxItems) : items} + items={items} columns={columns} /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx index be05e2b8418d6..feec289432a68 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx @@ -9,7 +9,6 @@ import { EuiCodeBlock, EuiFlexItem, EuiSpacer, - EuiDescriptionList, EuiTextColor, EuiFlyout, EuiFlyoutHeader, @@ -17,80 +16,49 @@ import { EuiFlyoutBody, EuiTabs, EuiTab, - EuiFlexGrid, - EuiCard, EuiFlexGroup, - EuiIcon, type PropsOf, EuiMarkdownFormat, } from '@elastic/eui'; import { assertNever } from '@kbn/std'; -import moment from 'moment'; import type { CspFinding } from '../types'; import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import * as TEXT from '../translations'; -import cisLogoIcon from '../../../assets/icons/cis_logo.svg'; -import k8sLogoIcon from '../../../assets/icons/k8s_logo.svg'; import { ResourceTab } from './resource_tab'; import { JsonTab } from './json_tab'; +import { OverviewTab } from './overview_tab'; +import { RuleTab } from './rule_tab'; const tabs = [ - { title: TEXT.REMEDIATION, id: 'remediation' }, + { title: TEXT.OVERVIEW, id: 'overview' }, + { title: TEXT.RULE, id: 'rule' }, { title: TEXT.RESOURCE, id: 'resource' }, - { title: TEXT.GENERAL, id: 'general' }, { title: TEXT.JSON, id: 'json' }, ] as const; -const CodeBlock: React.FC> = (props) => ( +export const CodeBlock: React.FC> = (props) => ( ); -const Markdown: React.FC> = (props) => ( +export const Markdown: React.FC> = (props) => ( ); type FindingsTab = typeof tabs[number]; -type EuiListItemsProps = NonNullable['listItems']>[number]; - -interface Card { - title: string; - listItems: Array<[EuiListItemsProps['title'], EuiListItemsProps['description']]>; -} - interface FindingFlyoutProps { onClose(): void; findings: CspFinding; } -const Cards = ({ data }: { data: Card[] }) => ( - - {data.map((card) => ( - - - ({ title: v[0], description: v[1] }))} - style={{ flexFlow: 'column' }} - descriptionProps={{ - style: { width: '100%' }, - }} - /> - - - ))} - -); - const FindingsTab = ({ tab, findings }: { findings: CspFinding; tab: FindingsTab }) => { switch (tab.id) { - case 'remediation': - return ; + case 'overview': + return ; + case 'rule': + return ; case 'resource': return ; - case 'general': - return ; case 'json': return ; default: @@ -131,55 +99,3 @@ export const FindingsRuleFlyout = ({ onClose, findings }: FindingFlyoutProps) => ); }; - -const getGeneralCards = ({ rule, ...rest }: CspFinding): Card[] => [ - { - title: TEXT.RULE, - listItems: [ - [TEXT.RULE_EVALUATED_AT, moment(rest['@timestamp']).format('MMMM D, YYYY @ HH:mm:ss.SSS')], - [ - TEXT.FRAMEWORK_SOURCES, - - - - - - - - , - ], - [TEXT.CIS_SECTION, rule.section], - [TEXT.PROFILE_APPLICABILITY, {rule.profile_applicability}], - [TEXT.BENCHMARK, rule.benchmark.name], - [TEXT.NAME, rule.name], - [TEXT.DESCRIPTION, {rule.description}], - [TEXT.AUDIT, {rule.audit}], - [TEXT.REFERENCES, {rule.references}], - ], - }, -]; - -const getRemediationCards = ({ result, rule, ...rest }: CspFinding): Card[] => [ - { - title: TEXT.RESULT_DETAILS, - listItems: [ - result.expected - ? [TEXT.EXPECTED, {JSON.stringify(result.expected, null, 2)}] - : ['', ''], - [TEXT.EVIDENCE, {JSON.stringify(result.evidence, null, 2)}], - [ - TEXT.RULE_EVALUATED_AT, - {moment(rest['@timestamp']).format('MMMM D, YYYY @ HH:mm:ss.SSS')}, - ], - ], - }, - { - title: TEXT.REMEDIATION, - listItems: [ - ['', {rule.remediation}], - [TEXT.IMPACT, {rule.impact}], - [TEXT.DEFAULT_VALUE, {rule.default_value}], - [TEXT.RATIONALE, {rule.rationale}], - ], - }, -]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/overview_tab.tsx new file mode 100644 index 0000000000000..679aea94935d8 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/overview_tab.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiAccordion, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import moment from 'moment'; +import type { EuiDescriptionListProps, EuiAccordionProps } from '@elastic/eui'; +import cisLogoIcon from '../../../assets/icons/cis_logo.svg'; +import k8sLogoIcon from '../../../assets/icons/k8s_logo.svg'; +import * as TEXT from '../translations'; +import { CspFinding } from '../types'; +import { CodeBlock, Markdown } from './findings_flyout'; + +type Accordion = Pick & + Pick; + +const getDetailsList = (data: CspFinding) => [ + { + title: TEXT.RULE_NAME, + description: data.rule.name, + }, + { + title: TEXT.EVALUATED_AT, + description: moment(data['@timestamp']).format('MMMM D, YYYY @ HH:mm:ss.SSS'), + }, + { + title: TEXT.RESOURCE_NAME, + description: data.resource.name, + }, + { + title: TEXT.FRAMEWORK_SOURCES, + description: ( + + + + + + + + + ), + }, + { + title: TEXT.CIS_SECTION, + description: data.rule.section, + }, +]; + +const getRemediationList = ({ rule }: CspFinding) => [ + { + title: '', + description: {rule.remediation}, + }, + { + title: TEXT.IMPACT, + description: {rule.impact}, + }, + { + title: TEXT.DEFAULT_VALUE, + description: {rule.default_value}, + }, + { + title: TEXT.RATIONALE, + description: {rule.rationale}, + }, +]; + +const getEvidenceList = ({ result }: CspFinding) => + [ + result.expected && { + title: TEXT.EXPECTED, + description: {JSON.stringify(result.expected, null, 2)}, + }, + { + title: TEXT.ACTUAL, + description: {JSON.stringify(result.evidence, null, 2)}, + }, + ].filter(Boolean) as EuiDescriptionListProps['listItems']; + +export const OverviewTab = ({ data }: { data: CspFinding }) => { + const accordions: Accordion[] = useMemo( + () => [ + { + initialIsOpen: true, + title: TEXT.DETAILS, + id: 'detailsAccordion', + listItems: getDetailsList(data), + }, + { + initialIsOpen: true, + title: TEXT.REMEDIATION, + id: 'remediationAccordion', + listItems: getRemediationList(data), + }, + { + initialIsOpen: false, + title: TEXT.EVIDENCE, + id: 'evidenceAccordion', + listItems: getEvidenceList(data), + }, + ], + [data] + ); + + return ( + <> + {accordions.map((accordion) => ( + + + + {accordion.title} + + } + arrowDisplay="left" + initialIsOpen={accordion.initialIsOpen} + > + + + + + + + ))} + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/resource_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/resource_tab.tsx index 7919b836a3a73..4ab801213a66d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/resource_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/resource_tab.tsx @@ -80,7 +80,7 @@ export const ResourceTab = ({ data }: { data: CspFinding }) => { {accordion.title} } - arrowDisplay="right" + arrowDisplay="left" initialIsOpen > [ + { + title: TEXT.NAME, + description: rule.name, + }, + { + title: TEXT.DESCRIPTION, + description: {rule.description}, + }, + { + title: TEXT.FRAMEWORK_SOURCES, + description: ( + + + + + + + + + ), + }, + { + title: TEXT.CIS_SECTION, + description: rule.section, + }, + { + title: TEXT.PROFILE_APPLICABILITY, + description: {rule.profile_applicability}, + }, + { + title: TEXT.BENCHMARK, + description: rule.benchmark.name, + }, + + { + title: TEXT.AUDIT, + description: {rule.audit}, + }, + { + title: TEXT.REFERENCES, + description: {rule.references}, + }, +]; + +export const RuleTab = ({ data }: { data: CspFinding }) => ( + +); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx index ae980c1e492bb..29c9df5f4a932 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.test.tsx @@ -18,6 +18,7 @@ import { useLocation } from 'react-router-dom'; import { RisonObject } from 'rison-node'; import { buildEsQuery } from '@kbn/es-query'; import { getFindingsCountAggQuery } from '../use_findings_count'; +import { getPaginationQuery } from '../utils'; jest.mock('../../../common/api/use_latest_findings_data_view'); jest.mock('../../../common/api/use_cis_kubernetes_integration'); @@ -69,9 +70,8 @@ describe('', () => { expect(dataMock.search.search).toHaveBeenNthCalledWith(2, { params: getFindingsQuery({ ...baseQuery, + ...getPaginationQuery(query), sort: query.sort, - size: query.size, - from: query.from, }), }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx index 78a1fd758b6ee..a013c3ef05f12 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_container.tsx @@ -7,8 +7,8 @@ import React, { useMemo } from 'react'; import { EuiSpacer } from '@elastic/eui'; import type { DataView } from '@kbn/data-plugin/common'; -import { SortDirection } from '@kbn/data-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; +import { number } from 'io-ts'; import { FindingsTable } from './latest_findings_table'; import { FindingsSearchBar } from '../layout/findings_search_bar'; import * as TEST_SUBJECTS from '../test_subjects'; @@ -18,7 +18,7 @@ import type { FindingsGroupByNoneQuery } from './use_latest_findings'; import type { FindingsBaseURLQuery } from '../types'; import { useFindingsCounter } from '../use_findings_count'; import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; -import { getBaseQuery } from '../utils'; +import { getBaseQuery, getPaginationQuery, getPaginationTableParams } from '../utils'; import { PageWrapper, PageTitle, PageTitleText } from '../layout/findings_layout'; import { FindingsGroupBySelector } from '../layout/findings_group_by_selector'; import { useCspBreadcrumbs } from '../../../common/navigation/use_csp_breadcrumbs'; @@ -27,14 +27,15 @@ import { findingsNavigation } from '../../../common/navigation/constants'; export const getDefaultQuery = (): FindingsBaseURLQuery & FindingsGroupByNoneQuery => ({ query: { language: 'kuery', query: '' }, filters: [], - sort: [{ ['@timestamp']: SortDirection.desc }], - from: 0, - size: 10, + sort: { field: '@timestamp', direction: 'desc' }, + pageIndex: 0, + pageSize: 10, }); export const LatestFindingsContainer = ({ dataView }: { dataView: DataView }) => { useCspBreadcrumbs([findingsNavigation.findings_default]); const { urlQuery, setUrlQuery } = useUrlQuery(getDefaultQuery); + const baseEsQuery = useMemo( () => getBaseQuery({ dataView, filters: urlQuery.filters, query: urlQuery.query }), [dataView, urlQuery.filters, urlQuery.query] @@ -43,11 +44,19 @@ export const LatestFindingsContainer = ({ dataView }: { dataView: DataView }) => const findingsCount = useFindingsCounter(baseEsQuery); const findingsGroupByNone = useLatestFindings({ ...baseEsQuery, - size: urlQuery.size, - from: urlQuery.from, + ...getPaginationQuery({ pageIndex: urlQuery.pageIndex, pageSize: urlQuery.pageSize }), sort: urlQuery.sort, }); + const findingsDistribution = getFindingsDistribution({ + total: findingsGroupByNone.data?.total, + passed: findingsCount.data?.passed, + failed: findingsCount.data?.failed, + pageIndex: urlQuery.pageIndex, + pageSize: urlQuery.pageSize, + currentPageSize: findingsGroupByNone.data?.page.length, + }); + return (
    setQuery={setUrlQuery} query={urlQuery.query} filters={urlQuery.filters} - loading={findingsGroupByNone.isLoading} + loading={findingsCount.isFetching} /> - - - } - /> - + - + {findingsDistribution && } + setUrlQuery({ pageIndex: page.index, pageSize: page.size, sort }) + } />
    ); }; + +const LatestFindingsPageTitle = () => ( + + } + /> + +); + +const getFindingsDistribution = ({ + total, + passed, + failed, + currentPageSize, + pageIndex, + pageSize, +}: Record<'currentPageSize' | 'total' | 'passed' | 'failed', number | undefined> & + Record<'pageIndex' | 'pageSize', number>) => { + if (!number.is(total) || !number.is(passed) || !number.is(failed) || !number.is(currentPageSize)) + return; + + return { + total, + passed, + failed, + pageStart: pageIndex * pageSize + 1, + pageEnd: pageIndex * pageSize + currentPageSize, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx index d01af2fa96e94..be2644be4bcdd 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx @@ -50,6 +50,7 @@ const getFakeFindings = (name: string): CspFinding & { id: string } => ({ version: chance.string(), }, resource: { + name: chance.string(), filename: chance.string(), type: chance.string(), path: chance.string(), @@ -70,10 +71,9 @@ describe('', () => { loading: false, data: { page: [], total: 0 }, error: null, - sort: [], - from: 1, - size: 10, - setQuery: jest.fn, + sorting: { sort: { field: '@timestamp', direction: 'desc' } }, + pagination: { pageSize: 10, pageIndex: 1, totalItemCount: 0 }, + setTableOptions: jest.fn(), }; render( @@ -93,10 +93,9 @@ describe('', () => { loading: false, data: { page: data, total: 10 }, error: null, - sort: [], - from: 0, - size: 10, - setQuery: jest.fn, + sorting: { sort: { field: '@timestamp', direction: 'desc' } }, + pagination: { pageSize: 10, pageIndex: 1, totalItemCount: 0 }, + setTableOptions: jest.fn(), }; render( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx index 6a2bd1c129b50..784bd3fa50767 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx @@ -4,38 +4,41 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { - type Criteria, EuiEmptyPrompt, EuiBasicTable, - EuiBasicTableProps, EuiBasicTableColumn, + type Pagination, + type EuiBasicTableProps, + type CriteriaWithPagination, + type EuiTableActionsColumnType, } from '@elastic/eui'; -import { SortDirection } from '@kbn/data-plugin/common'; -import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types'; import { extractErrorMessage } from '../../../../common/utils/helpers'; import * as TEST_SUBJECTS from '../test_subjects'; import * as TEXT from '../translations'; import type { CspFinding } from '../types'; -import type { FindingsGroupByNoneQuery, CspFindingsResult } from './use_latest_findings'; +import type { CspFindingsResult } from './use_latest_findings'; import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; import { getExpandColumn, getFindingsColumns } from '../layout/findings_layout'; -interface BaseFindingsTableProps extends FindingsGroupByNoneQuery { - setQuery(query: Partial): void; +type TableProps = Required>; + +interface BaseFindingsTableProps { + pagination: Pagination; + sorting: TableProps['sorting']; + setTableOptions(options: CriteriaWithPagination): void; } type FindingsTableProps = CspFindingsResult & BaseFindingsTableProps; const FindingsTableComponent = ({ - setQuery, - from, - size, - sort = [], error, data, loading, + pagination, + sorting, + setTableOptions, }: FindingsTableProps) => { const [selectedFinding, setSelectedFinding] = useState(); @@ -47,25 +50,6 @@ const FindingsTableComponent = ({ [] ); - const pagination = useMemo( - () => - getEuiPaginationFromEsSearchSource({ - from, - size, - total: data?.total, - }), - [from, size, data] - ); - - const sorting = useMemo(() => getEuiSortFromEsSearchSource(sort), [sort]); - - const onTableChange = useCallback( - (params: Criteria) => { - setQuery(getEsSearchQueryFromEuiTableParams(params)); - }, - [setQuery] - ); - // Show "zero state" if (!loading && !data?.page.length) // TODO: use our own logo @@ -87,7 +71,7 @@ const FindingsTableComponent = ({ columns={columns} pagination={pagination} sorting={sorting} - onChange={onTableChange} + onChange={setTableOptions} hasActions /> {selectedFinding && ( @@ -100,38 +84,4 @@ const FindingsTableComponent = ({ ); }; -const getEuiPaginationFromEsSearchSource = ({ - from: pageIndex, - size: pageSize, - total, -}: Pick & { - total: number | undefined; -}): EuiBasicTableProps['pagination'] => ({ - pageSize, - pageIndex: Math.ceil(pageIndex / pageSize), - totalItemCount: total || 0, - pageSizeOptions: [10, 25, 100], - showPerPageOptions: true, -}); - -const getEuiSortFromEsSearchSource = ( - sort: FindingsGroupByNoneQuery['sort'] -): EuiBasicTableProps['sorting'] => { - if (!sort.length) return; - - const entry = Object.entries(sort[0])?.[0]; - if (!entry) return; - - const [field, direction] = entry; - return { sort: { field: field as keyof CspFinding, direction: direction as SortDirection } }; -}; - -const getEsSearchQueryFromEuiTableParams = ({ - page, - sort, -}: Criteria): Partial => ({ - ...(!!page && { from: page.index * page.size, size: page.size }), - sort: sort ? [{ [sort.field]: SortDirection[sort.direction] }] : undefined, -}); - export const FindingsTable = React.memo(FindingsTableComponent); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/use_latest_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/use_latest_findings.ts index 608f400953c86..e7f9655849be0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/use_latest_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/use_latest_findings.ts @@ -7,8 +7,9 @@ import { useQuery } from 'react-query'; import { number } from 'io-ts'; import { lastValueFrom } from 'rxjs'; -import type { EsQuerySortValue, IEsSearchResponse } from '@kbn/data-plugin/common'; +import type { IEsSearchResponse } from '@kbn/data-plugin/common'; import type { CoreStart } from '@kbn/core/public'; +import type { Criteria, Pagination } from '@elastic/eui'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { extractErrorMessage } from '../../../../common/utils/helpers'; import * as TEXT from '../translations'; @@ -16,12 +17,18 @@ import type { CspFinding, FindingsQueryResult } from '../types'; import { useKibana } from '../../../common/hooks/use_kibana'; import type { FindingsBaseEsQuery } from '../types'; -interface UseFindingsOptions extends FindingsBaseEsQuery, FindingsGroupByNoneQuery {} - -export interface FindingsGroupByNoneQuery { +interface UseFindingsOptions extends FindingsBaseEsQuery { from: NonNullable; size: NonNullable; - sort: EsQuerySortValue[]; + sort: Sort; +} + +type Sort = NonNullable['sort']>; + +export interface FindingsGroupByNoneQuery { + pageIndex: Pagination['pageIndex']; + pageSize: Pagination['pageSize']; + sort: Sort; } interface CspFindingsData { @@ -37,23 +44,6 @@ const FIELDS_WITHOUT_KEYWORD_MAPPING = new Set(['@timestamp']); const getSortKey = (key: string): string => FIELDS_WITHOUT_KEYWORD_MAPPING.has(key) ? key : `${key}.keyword`; -/** - * @description utility to transform a column header key to its field mapping for sorting - * @example Adds '.keyword' to every property we sort on except values of `FIELDS_WITHOUT_KEYWORD_MAPPING` - * @todo find alternative - * @note we choose the keyword 'keyword' in the field mapping - */ -const mapEsQuerySortKey = (sort: readonly EsQuerySortValue[]): EsQuerySortValue[] => - sort.slice().reduce((acc, cur) => { - const entry = Object.entries(cur)[0]; - if (!entry) return acc; - - const [k, v] = entry; - acc.push({ [getSortKey(k)]: v }); - - return acc; - }, []); - export const showErrorToast = ( toasts: CoreStart['notifications']['toasts'], error: unknown @@ -67,7 +57,7 @@ export const getFindingsQuery = ({ index, query, size, from, sort }: UseFindings query, size, from, - sort: mapEsQuerySortKey(sort), + sort: [{ [getSortKey(sort.field)]: sort.direction }], }); export const useLatestFindings = ({ index, query, sort, from, size }: UseFindingsOptions) => { @@ -85,6 +75,7 @@ export const useLatestFindings = ({ index, query, sort, from, size }: UseFinding }) ), { + keepPreviousData: true, select: ({ rawResponse: { hits } }) => ({ page: hits.hits.map((hit) => hit._source!), total: number.is(hits.total) ? hits.total : 0, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx index a48926b3653aa..e1794a92d7b92 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx @@ -15,30 +15,30 @@ import * as TEST_SUBJECTS from '../../test_subjects'; import { PageWrapper, PageTitle, PageTitleText } from '../../layout/findings_layout'; import { useCspBreadcrumbs } from '../../../../common/navigation/use_csp_breadcrumbs'; import { findingsNavigation } from '../../../../common/navigation/constants'; -import { useResourceFindings } from './use_resource_findings'; +import { ResourceFindingsQuery, useResourceFindings } from './use_resource_findings'; import { useUrlQuery } from '../../../../common/hooks/use_url_query'; import type { FindingsBaseURLQuery } from '../../types'; -import { getBaseQuery } from '../../utils'; +import { getBaseQuery, getPaginationQuery, getPaginationTableParams } from '../../utils'; import { ResourceFindingsTable } from './resource_findings_table'; import { FindingsSearchBar } from '../../layout/findings_search_bar'; -const getDefaultQuery = (): FindingsBaseURLQuery => ({ +const getDefaultQuery = (): FindingsBaseURLQuery & ResourceFindingsQuery => ({ query: { language: 'kuery', query: '' }, filters: [], + pageIndex: 0, + pageSize: 10, }); -const BackToResourcesButton = () => { - return ( - - - - - - ); -}; +const BackToResourcesButton = () => ( + + + + + +); export const ResourceFindings = ({ dataView }: { dataView: DataView }) => { useCspBreadcrumbs([findingsNavigation.findings_default]); @@ -47,8 +47,16 @@ export const ResourceFindings = ({ dataView }: { dataView: DataView }) => { const { urlQuery, setUrlQuery } = useUrlQuery(getDefaultQuery); const resourceFindings = useResourceFindings({ - ...getBaseQuery({ dataView, filters: urlQuery.filters, query: urlQuery.query }), resourceId: params.resourceId, + ...getBaseQuery({ + dataView, + filters: urlQuery.filters, + query: urlQuery.query, + }), + ...getPaginationQuery({ + pageSize: urlQuery.pageSize, + pageIndex: urlQuery.pageIndex, + }), }); return ( @@ -58,7 +66,7 @@ export const ResourceFindings = ({ dataView }: { dataView: DataView }) => { setQuery={setUrlQuery} query={urlQuery.query} filters={urlQuery.filters} - loading={resourceFindings.isLoading} + loading={resourceFindings.isFetching} /> @@ -77,9 +85,17 @@ export const ResourceFindings = ({ dataView }: { dataView: DataView }) => { + setUrlQuery({ pageIndex: page.index, pageSize: page.size }) + } />
    diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx index ec04d05109cdd..8300cdd503fee 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx @@ -5,17 +5,27 @@ * 2.0. */ import React from 'react'; -import { EuiEmptyPrompt, EuiBasicTable } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiBasicTable, CriteriaWithPagination, Pagination } from '@elastic/eui'; import { extractErrorMessage } from '../../../../../common/utils/helpers'; import * as TEXT from '../../translations'; import type { ResourceFindingsResult } from './use_resource_findings'; import { getFindingsColumns } from '../../layout/findings_layout'; +import type { CspFinding } from '../../types'; -type FindingsGroupByResourceProps = ResourceFindingsResult; +interface Props extends ResourceFindingsResult { + pagination: Pagination; + setTableOptions(options: CriteriaWithPagination): void; +} const columns = getFindingsColumns(); -const ResourceFindingsTableComponent = ({ error, data, loading }: FindingsGroupByResourceProps) => { +const ResourceFindingsTableComponent = ({ + error, + data, + loading, + pagination, + setTableOptions, +}: Props) => { if (!loading && !data?.page.length) return {TEXT.NO_FINDINGS}
    } />; @@ -25,6 +35,8 @@ const ResourceFindingsTableComponent = ({ error, data, loading }: FindingsGroupB error={error ? extractErrorMessage(error) : undefined} items={data?.page || []} columns={columns} + onChange={setTableOptions} + pagination={pagination} /> ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts index 7123b80ef0228..9e015d84e2043 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts @@ -8,12 +8,20 @@ import { useQuery } from 'react-query'; import { lastValueFrom } from 'rxjs'; import { IEsSearchResponse } from '@kbn/data-plugin/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Pagination } from '@elastic/eui'; import { useKibana } from '../../../../common/hooks/use_kibana'; import { showErrorToast } from '../../latest_findings/use_latest_findings'; import type { CspFinding, FindingsBaseEsQuery, FindingsQueryResult } from '../../types'; interface UseResourceFindingsOptions extends FindingsBaseEsQuery { resourceId: string; + from: NonNullable; + size: NonNullable; +} + +export interface ResourceFindingsQuery { + pageIndex: Pagination['pageIndex']; + pageSize: Pagination['pageSize']; } export type ResourceFindingsResult = FindingsQueryResult< @@ -21,12 +29,16 @@ export type ResourceFindingsResult = FindingsQueryResult< unknown >; -export const getResourceFindingsQuery = ({ +const getResourceFindingsQuery = ({ index, query, resourceId, + from, + size, }: UseResourceFindingsOptions): estypes.SearchRequest => ({ index, + from, + size, body: { query: { ...query, @@ -38,21 +50,28 @@ export const getResourceFindingsQuery = ({ }, }); -export const useResourceFindings = ({ index, query, resourceId }: UseResourceFindingsOptions) => { +export const useResourceFindings = ({ + index, + query, + resourceId, + from, + size, +}: UseResourceFindingsOptions) => { const { data, notifications: { toasts }, } = useKibana().services; return useQuery( - ['csp_resource_findings', { index, query, resourceId }], + ['csp_resource_findings', { index, query, resourceId, from, size }], () => lastValueFrom>( data.search.search({ - params: getResourceFindingsQuery({ index, query, resourceId }), + params: getResourceFindingsQuery({ index, query, resourceId, from, size }), }) ), { + keepPreviousData: true, select: ({ rawResponse: { hits } }) => ({ page: hits.hits.map((hit) => hit._source!), total: hits.total as number, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx index 0654f7d9f0999..c155b1cae7eda 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_distribution_bar.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { css } from '@emotion/react'; import { EuiHealth, @@ -29,35 +29,25 @@ interface Props { const formatNumber = (value: number) => (value < 1000 ? value : numeral(value).format('0.0a')); -export const FindingsDistributionBar = ({ failed, passed, total, pageEnd, pageStart }: Props) => { - const count = useMemo( - () => - total - ? { total, passed: passed / total, failed: failed / total } - : { total: 0, passed: 0, failed: 0 }, - [total, failed, passed] - ); - - return ( -
    - - - -
    - ); -}; +export const FindingsDistributionBar = (props: Props) => ( +
    + + + +
    +); const Counters = ({ pageStart, pageEnd, total, failed, passed }: Props) => ( - {!!total && } + - {!!total && } + ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx index ee1a00abc4901..1374f98b45933 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx @@ -16,6 +16,7 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; import moment from 'moment'; +import { i18n } from '@kbn/i18n'; import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import * as TEXT from '../translations'; import { CspFinding } from '../types'; @@ -52,8 +53,10 @@ export const getExpandColumn = ({ width: '40px', actions: [ { - name: 'Expand', - description: 'Expand', + name: i18n.translate('xpack.csp.expandColumnNameLabel', { defaultMessage: 'Expand' }), + description: i18n.translate('xpack.csp.expandColumnDescriptionLabel', { + defaultMessage: 'Expand', + }), type: 'icon', icon: 'expand', onClick, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts index 53d4b8a86e5c0..6301625f9bba1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts @@ -42,10 +42,18 @@ export const RESOURCE = i18n.translate('xpack.csp.findings.resourceLabel', { defaultMessage: 'Resource', }); +export const RESOURCE_NAME = i18n.translate('xpack.csp.findings.resourceNameLabel', { + defaultMessage: 'Resource Name', +}); + export const GENERAL = i18n.translate('xpack.csp.findings.findingsFlyout.generalTabLabel', { defaultMessage: 'General', }); +export const OVERVIEW = i18n.translate('xpack.csp.findings.findingsFlyout.overviewTabLabel', { + defaultMessage: 'Overview', +}); + export const JSON = i18n.translate('xpack.csp.findings.findingsFlyout.jsonTabLabel', { defaultMessage: 'JSON', }); @@ -137,6 +145,10 @@ export const RULE_EVALUATED_AT = i18n.translate('xpack.csp.findings.ruleEvaluate defaultMessage: 'Rule evaluated at', }); +export const EVALUATED_AT = i18n.translate('xpack.csp.findings.evaluatedAt', { + defaultMessage: 'Evaluated at', +}); + export const FRAMEWORK_SOURCES = i18n.translate('xpack.csp.findings.frameworkSourcesLabel', { defaultMessage: 'Framework Sources', }); @@ -173,6 +185,10 @@ export const EXPECTED = i18n.translate('xpack.csp.findings.expectedLabel', { defaultMessage: 'Expected', }); +export const ACTUAL = i18n.translate('xpack.csp.findings.actualLabel', { + defaultMessage: 'Actual', +}); + export const EVIDENCE = i18n.translate('xpack.csp.findings.evidenceLabel', { defaultMessage: 'Evidence', }); @@ -237,7 +253,11 @@ export const NO_FINDINGS = i18n.translate('xpack.csp.findings.nonFindingsLabel', defaultMessage: 'There are no Findings', }); +export const DETAILS = i18n.translate('xpack.csp.findings.findingsFlyout.detailsTabLabel', { + defaultMessage: 'Details', +}); + export const FINDINGS_SEARCH_PLACEHOLDER = i18n.translate( 'xpack.csp.findings.searchBar.searchPlaceholder', - { defaultMessage: 'Search findings (eg. resource.section : "API Server")' } + { defaultMessage: 'Search findings (eg. rule.section.keyword : "API Server" )' } ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts index 9fed484a88128..57646c6883abb 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts @@ -69,6 +69,7 @@ interface CspFindingResource { mode: string; path: string; type: string; + name: string; [other_keys: string]: unknown; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/use_findings_count.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/use_findings_count.ts index f48e630b489d4..a63a3fac32c8b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/use_findings_count.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/use_findings_count.ts @@ -48,6 +48,7 @@ export const useFindingsCounter = ({ index, query }: FindingsBaseEsQuery) => { }) ), { + keepPreviousData: true, onError: (err) => showErrorToast(toasts, err), select: (response) => Object.fromEntries( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts index d3281a1a0dbc8..5f4d574930370 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts @@ -7,6 +7,7 @@ import { buildEsQuery } from '@kbn/es-query'; import type { DataView } from '@kbn/data-plugin/common'; +import { EuiBasicTableProps, Pagination } from '@elastic/eui'; import type { FindingsBaseEsQuery, FindingsBaseURLQuery } from './types'; export const getBaseQuery = ({ @@ -20,3 +21,23 @@ export const getBaseQuery = ({ // will be accounted for before releasing the feature query: buildEsQuery(dataView, query, filters), }); + +type TablePagination = NonNullable['pagination']>; + +export const getPaginationTableParams = ( + params: TablePagination & Pick, 'pageIndex' | 'pageSize'>, + pageSizeOptions = [10, 25, 100], + showPerPageOptions = true +): Required => ({ + ...params, + pageSizeOptions, + showPerPageOptions, +}); + +export const getPaginationQuery = ({ + pageIndex, + pageSize, +}: Pick) => ({ + from: pageIndex * pageSize, + size: pageSize, +}); 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/encrypted_saved_objects/README.md b/x-pack/plugins/encrypted_saved_objects/README.md index 41e2dce75da15..dac76f51cbc75 100644 --- a/x-pack/plugins/encrypted_saved_objects/README.md +++ b/x-pack/plugins/encrypted_saved_objects/README.md @@ -99,6 +99,21 @@ const savedObjectWithDecryptedContent = await esoClient.getDecryptedAsInternalU one would pass to `SavedObjectsClient.get`. These argument allows to specify `namespace` property that, for example, is required if Saved Object was created within a non-default space. +Alternative option is using `createPointInTimeFinderDecryptedAsInternalUser` API method, that can be used to help page through large sets of saved objects. +Its interface matches interface of the corresponding Saved Objects API `createPointInTimeFinder` method: + +```typescript +const finder = await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser({ + filter, + type: 'my-saved-object-type', + perPage: 1000, +}); + +for await (const response of finder.find()) { + // process response +} +``` + ### Defining migrations EncryptedSavedObjects rely on standard SavedObject migrations, but due to the additional complexity introduced by the need to decrypt and reencrypt the migrated document, there are some caveats to how we support this. The good news is, most of this complexity is abstracted away by the plugin and all you need to do is leverage our api. diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 5f80e5bab310f..aa72fb372e878 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -25,18 +25,19 @@ function createEncryptedSavedObjectsSetupMock( function createEncryptedSavedObjectsStartMock() { return { isEncryptionError: jest.fn(), - getClient: jest.fn((opts) => createEncryptedSavedObjectsClienttMock(opts)), + getClient: jest.fn((opts) => createEncryptedSavedObjectsClientMock(opts)), } as jest.Mocked; } -function createEncryptedSavedObjectsClienttMock(opts?: EncryptedSavedObjectsClientOptions) { +function createEncryptedSavedObjectsClientMock(opts?: EncryptedSavedObjectsClientOptions) { return { getDecryptedAsInternalUser: jest.fn(), + createPointInTimeFinderDecryptedAsInternalUser: jest.fn(), } as jest.Mocked; } export const encryptedSavedObjectsMock = { createSetup: createEncryptedSavedObjectsSetupMock, createStart: createEncryptedSavedObjectsStartMock, - createClient: createEncryptedSavedObjectsClienttMock, + createClient: createEncryptedSavedObjectsClientMock, }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index f4708e182ad31..970f3baed7ab1 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -60,10 +60,11 @@ describe('EncryptedSavedObjects Plugin', () => { `); expect(startContract.getClient()).toMatchInlineSnapshot(` - Object { - "getDecryptedAsInternalUser": [Function], - } - `); + Object { + "createPointInTimeFinderDecryptedAsInternalUser": [Function], + "getDecryptedAsInternalUser": [Function], + } + `); }); }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts index 032842e0047c0..b93141a3ad989 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts @@ -166,5 +166,171 @@ describe('#setupSavedObjects', () => { { namespace: 'some-ns' } ); }); + + it('does not call decryptAttributes if Saved Object type is not registered', async () => { + const mockSavedObject: SavedObject = { + id: 'some-id', + type: 'not-known-type', + attributes: { attrOne: 'one', attrSecret: '*secret*' }, + references: [], + }; + mockSavedObjectsRepository.get.mockResolvedValue(mockSavedObject); + + await expect( + setupContract().getDecryptedAsInternalUser(mockSavedObject.type, mockSavedObject.id, { + namespace: 'some-ns', + }) + ).resolves.toEqual(mockSavedObject); + + expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledTimes(0); + }); + }); + + describe('#createPointInTimeFinderDecryptedAsInternalUser', () => { + it('includes `namespace` for single-namespace saved objects', async () => { + const mockSavedObject: SavedObject = { + id: 'some-id', + type: 'known-type', + attributes: { attrOne: 'one', attrSecret: '*secret*' }, + references: [], + }; + mockSavedObjectsRepository.createPointInTimeFinder = jest.fn().mockReturnValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [mockSavedObject] }; + }, + }); + + mockSavedObjectTypeRegistry.isSingleNamespace.mockReturnValue(true); + + const finder = await setupContract().createPointInTimeFinderDecryptedAsInternalUser({ + type: 'known-type', + namespaces: ['some-ns'], + }); + + for await (const res of finder.find()) { + expect(res).toEqual({ + saved_objects: [ + { + ...mockSavedObject, + attributes: { attrOne: 'one', attrSecret: 'secret' }, + }, + ], + }); + } + + expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledTimes(1); + expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledWith( + { type: mockSavedObject.type, id: mockSavedObject.id, namespace: 'some-ns' }, + mockSavedObject.attributes + ); + + expect(mockSavedObjectsRepository.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsRepository.createPointInTimeFinder).toHaveBeenCalledWith( + { type: 'known-type', namespaces: ['some-ns'] }, + undefined + ); + }); + + it('does not include `namespace` for multiple-namespace saved objects', async () => { + const mockSavedObject: SavedObject = { + id: 'some-id', + type: 'known-type', + attributes: { attrOne: 'one', attrSecret: '*secret*' }, + references: [], + }; + mockSavedObjectsRepository.createPointInTimeFinder = jest.fn().mockReturnValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [mockSavedObject] }; + }, + }); + + mockSavedObjectTypeRegistry.isSingleNamespace.mockReturnValue(false); + + const finder = await setupContract().createPointInTimeFinderDecryptedAsInternalUser({ + type: 'known-type', + namespaces: ['some-ns'], + }); + + for await (const res of finder.find()) { + expect(res).toEqual({ + saved_objects: [ + { + ...mockSavedObject, + attributes: { attrOne: 'one', attrSecret: 'secret' }, + }, + ], + }); + } + + expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledTimes(1); + expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledWith( + { type: mockSavedObject.type, id: mockSavedObject.id, namespace: undefined }, + mockSavedObject.attributes + ); + + expect(mockSavedObjectsRepository.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsRepository.createPointInTimeFinder).toHaveBeenCalledWith( + { type: 'known-type', namespaces: ['some-ns'] }, + undefined + ); + }); + + it('does not call decryptAttributes if Saved Object type is not registered', async () => { + const mockSavedObject: SavedObject = { + id: 'some-id', + type: 'not-known-type', + attributes: { attrOne: 'one', attrSecret: '*secret*' }, + references: [], + }; + mockSavedObjectsRepository.createPointInTimeFinder = jest.fn().mockReturnValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [mockSavedObject] }; + }, + }); + + const finder = await setupContract().createPointInTimeFinderDecryptedAsInternalUser({ + type: 'not-known-type', + namespaces: ['some-ns'], + }); + + for await (const res of finder.find()) { + expect(res).toEqual({ + saved_objects: [mockSavedObject], + }); + } + + expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledTimes(0); + }); + + it('returns error within Saved Object if decryption failed', async () => { + const mockSavedObject: SavedObject = { + id: 'some-id', + type: 'known-type', + attributes: { attrOne: 'one', attrSecret: '*secret*' }, + references: [], + }; + mockSavedObjectsRepository.createPointInTimeFinder = jest.fn().mockReturnValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [mockSavedObject] }; + }, + }); + + mockEncryptedSavedObjectsService.decryptAttributes.mockImplementation(() => { + throw new Error('Test failure'); + }); + + const finder = await setupContract().createPointInTimeFinderDecryptedAsInternalUser({ + type: 'known-type', + namespaces: ['some-ns'], + }); + + for await (const res of finder.find()) { + expect(res.saved_objects[0].error).toHaveProperty('message', 'Test failure'); + } + }); }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts index 3d9d36206b5c9..e2b58e3003d96 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts @@ -5,11 +5,16 @@ * 2.0. */ +import pMap from 'p-map'; + import type { + ISavedObjectsPointInTimeFinder, ISavedObjectsRepository, ISavedObjectTypeRegistry, SavedObject, SavedObjectsBaseOptions, + SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsServiceSetup, StartServicesAccessor, } from '@kbn/core/server'; @@ -43,6 +48,31 @@ export interface EncryptedSavedObjectsClient { id: string, options?: SavedObjectsBaseOptions ) => Promise>; + + /** + * API method, that can be used to help page through large sets of saved objects and returns decrypted properties in result SO. + * Its interface matches interface of the corresponding Saved Objects API `createPointInTimeFinder` method: + * + * @example + * ```ts + * const finder = await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser({ + * filter, + * type: 'my-saved-object-type', + * perPage: 1000, + * }); + * for await (const response of finder.find()) { + * // process response + * } + * ``` + * + * @param findOptions matches interface of corresponding argument of Saved Objects API `createPointInTimeFinder` {@link SavedObjectsCreatePointInTimeFinderOptions} + * @param dependencies matches interface of corresponding argument of Saved Objects API `createPointInTimeFinder` {@link SavedObjectsCreatePointInTimeFinderDependencies} + * + */ + createPointInTimeFinderDecryptedAsInternalUser( + findOptions: SavedObjectsCreatePointInTimeFinderOptions, + dependencies?: SavedObjectsCreatePointInTimeFinderDependencies + ): Promise>; } export function setupSavedObjects({ @@ -84,6 +114,11 @@ export function setupSavedObjects({ ): Promise> => { const [internalRepository, typeRegistry] = await internalRepositoryAndTypeRegistryPromise; const savedObject = await internalRepository.get(type, id, options); + + if (!service.isRegistered(savedObject.type)) { + return savedObject as SavedObject; + } + return { ...savedObject, attributes: (await service.decryptAttributes( @@ -96,6 +131,61 @@ export function setupSavedObjects({ )) as T, }; }, + + createPointInTimeFinderDecryptedAsInternalUser: async ( + findOptions: SavedObjectsCreatePointInTimeFinderOptions, + dependencies?: SavedObjectsCreatePointInTimeFinderDependencies + ): Promise> => { + const [internalRepository, typeRegistry] = await internalRepositoryAndTypeRegistryPromise; + const finder = internalRepository.createPointInTimeFinder(findOptions, dependencies); + const finderAsyncGenerator = finder.find(); + + async function* encryptedFinder() { + for await (const res of finderAsyncGenerator) { + const encryptedSavedObjects = await pMap( + res.saved_objects, + async (savedObject) => { + if (!service.isRegistered(savedObject.type)) { + return savedObject; + } + + const descriptor = { + type: savedObject.type, + id: savedObject.id, + namespace: getDescriptorNamespace( + typeRegistry, + savedObject.type, + findOptions.namespaces + ), + }; + + try { + return { + ...savedObject, + attributes: (await service.decryptAttributes( + descriptor, + savedObject.attributes as Record + )) as T, + }; + } catch (error) { + // catch error and enrich SO with it, return stripped attributes. Then consumer of API can decide either proceed + // with only unsecured properties or stop when error happens + const { attributes: strippedAttrs } = await service.stripOrDecryptAttributes( + descriptor, + savedObject.attributes as Record + ); + return { ...savedObject, attributes: strippedAttrs as T, error }; + } + }, + { concurrency: 50 } + ); + + yield { ...res, saved_objects: encryptedSavedObjects }; + } + } + + return { ...finder, find: () => encryptedFinder() }; + }, }; }; } 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/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index dcc7092356a96..7b185960dcb7b 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -107,6 +107,7 @@ export const AGENT_API_ROUTES = { CHECKIN_PATTERN: `${API_ROOT}/agents/{agentId}/checkin`, ACKS_PATTERN: `${API_ROOT}/agents/{agentId}/acks`, ACTIONS_PATTERN: `${API_ROOT}/agents/{agentId}/actions`, + CANCEL_ACTIONS_PATTERN: `${API_ROOT}/agents/actions/{actionId}/cancel`, UNENROLL_PATTERN: `${API_ROOT}/agents/{agentId}/unenroll`, BULK_UNENROLL_PATTERN: `${API_ROOT}/agents/bulk_unenroll`, REASSIGN_PATTERN: `${API_ROOT}/agents/{agentId}/reassign`, @@ -117,6 +118,7 @@ export const AGENT_API_ROUTES = { STATUS_PATTERN_DEPRECATED: `${API_ROOT}/agent-status`, UPGRADE_PATTERN: `${API_ROOT}/agents/{agentId}/upgrade`, BULK_UPGRADE_PATTERN: `${API_ROOT}/agents/bulk_upgrade`, + CURRENT_UPGRADES_PATTERN: `${API_ROOT}/agents/current_upgrades`, }; export const ENROLLMENT_API_KEY_ROUTES = { diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index b3c37f5e567c3..dca3fd3ccb678 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -3795,6 +3795,9 @@ "source_uri": { "type": "string" }, + "rollout_duration_seconds": { + "type": "number" + }, "agents": { "oneOf": [ { diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 36175ffc59a88..d1a114b35ab6c 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -2391,6 +2391,8 @@ components: type: string source_uri: type: string + rollout_duration_seconds: + type: number agents: oneOf: - type: array diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/bulk_upgrade_agents.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/bulk_upgrade_agents.yaml index 31209d43fb58d..74df244983a84 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/bulk_upgrade_agents.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/bulk_upgrade_agents.yaml @@ -5,6 +5,8 @@ properties: type: string source_uri: type: string + rollout_duration_seconds: + type: number agents: oneOf: - type: array diff --git a/x-pack/plugins/fleet/common/services/get_max_version.test.ts b/x-pack/plugins/fleet/common/services/get_max_version.test.ts new file mode 100644 index 0000000000000..6b21c81c0a5fe --- /dev/null +++ b/x-pack/plugins/fleet/common/services/get_max_version.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMaxVersion } from './get_max_version'; + +describe('Fleet - getMaxVersion', () => { + it('returns the maximum version', () => { + const versions = ['8.1.0', '8.3.0', '8.2.1', '7.16.0', '8.2.0', '7.16.1', '8.3.1']; + expect(getMaxVersion(versions)).toEqual('8.3.1'); + }); + + it('returns the maximum version when there are duplicates', () => { + const versions = ['8.1.0', '8.3.0', '8.2.1', '7.16.0', '8.2.0', '7.16.1', '8.2.0', '7.15.1']; + expect(getMaxVersion(versions)).toEqual('8.3.0'); + }); + + it('returns the maximum version when there is a snapshot version', () => { + const versions = ['8.1.0', '8.2.0-SNAPSHOT', '7.16.0', '7.16.1']; + expect(getMaxVersion(versions)).toEqual('8.2.0-SNAPSHOT'); + }); + + it('returns the maximum version and prefers the major version to the snapshot', () => { + const versions = ['8.1.0', '8.2.0-SNAPSHOT', '8.2.0', '7.16.0', '7.16.1']; + expect(getMaxVersion(versions)).toEqual('8.2.0'); + }); + + it('when there is only a version returns it', () => { + const versions = ['8.1.0']; + expect(getMaxVersion(versions)).toEqual('8.1.0'); + }); + + it('returns an empty string when the passed array is empty', () => { + const versions: string[] = []; + expect(getMaxVersion(versions)).toEqual(''); + }); +}); diff --git a/x-pack/plugins/fleet/common/services/get_max_version.ts b/x-pack/plugins/fleet/common/services/get_max_version.ts new file mode 100644 index 0000000000000..e34dec675999d --- /dev/null +++ b/x-pack/plugins/fleet/common/services/get_max_version.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { uniq } from 'lodash'; +import semverGt from 'semver/functions/gt'; +import semverCoerce from 'semver/functions/coerce'; + +// Find max version from an array of string versions +export function getMaxVersion(versions: string[]) { + const uniqVersions: string[] = uniq(versions); + + if (uniqVersions.length === 1) { + const semverVersion = semverCoerce(uniqVersions[0])?.version; + return semverVersion ? semverVersion : ''; + } else if (uniqVersions.length > 1) { + const sorted = uniqVersions.sort((a, b) => (semverGt(a, b) ? 1 : -1)); + return sorted[sorted.length - 1]; + } + return ''; +} diff --git a/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts b/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts index 975d45fd01c64..3a4676a4f9a7f 100644 --- a/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts @@ -241,6 +241,7 @@ describe('Fleet - validatePackagePolicy()', () => { ], }, ], + vars: {}, }; const invalidPackagePolicy: NewPackagePolicy = { @@ -332,6 +333,7 @@ describe('Fleet - validatePackagePolicy()', () => { ], }, ], + vars: {}, }; const noErrorsValidationResults = { @@ -370,6 +372,7 @@ describe('Fleet - validatePackagePolicy()', () => { vars: { 'var-name': null }, }, }, + vars: {}, }; it('returns no errors for valid package policy', () => { @@ -416,6 +419,7 @@ describe('Fleet - validatePackagePolicy()', () => { streams: { 'with-no-stream-vars-bar': {} }, }, }, + vars: {}, }); }); @@ -487,6 +491,7 @@ describe('Fleet - validatePackagePolicy()', () => { streams: { 'with-no-stream-vars-bar': {} }, }, }, + vars: {}, }); }); @@ -505,6 +510,7 @@ describe('Fleet - validatePackagePolicy()', () => { description: null, namespace: null, inputs: null, + vars: {}, }); expect( validatePackagePolicy( @@ -520,6 +526,7 @@ describe('Fleet - validatePackagePolicy()', () => { description: null, namespace: null, inputs: null, + vars: {}, }); }); @@ -538,6 +545,7 @@ describe('Fleet - validatePackagePolicy()', () => { description: null, namespace: null, inputs: null, + vars: {}, }); expect( validatePackagePolicy( @@ -553,6 +561,7 @@ describe('Fleet - validatePackagePolicy()', () => { description: null, namespace: null, inputs: null, + vars: {}, }); }); @@ -604,6 +613,7 @@ describe('Fleet - validatePackagePolicy()', () => { }, }, }, + vars: {}, }); }); @@ -729,6 +739,7 @@ describe('Fleet - validatePackagePolicy()', () => { }, }, }, + vars: {}, name: null, namespace: null, }); diff --git a/x-pack/plugins/fleet/common/services/validate_package_policy.ts b/x-pack/plugins/fleet/common/services/validate_package_policy.ts index 4d54fda6e5df5..3d0e8bed2aafa 100644 --- a/x-pack/plugins/fleet/common/services/validate_package_policy.ts +++ b/x-pack/plugins/fleet/common/services/validate_package_policy.ts @@ -55,6 +55,7 @@ export const validatePackagePolicy = ( description: null, namespace: null, inputs: {}, + vars: {}, }; const namespaceValidation = isValidNamespace(packagePolicy.namespace); diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index d41a08b8b4755..85d17cf67cfd1 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -34,7 +34,8 @@ export type AgentActionType = | 'UNENROLL' | 'UPGRADE' | 'SETTINGS' - | 'POLICY_REASSIGN'; + | 'POLICY_REASSIGN' + | 'CANCEL'; export interface NewAgentAction { type: AgentActionType; @@ -44,6 +45,9 @@ export interface NewAgentAction { agents: string[]; created_at?: string; id?: string; + expiration?: string; + start_time?: string; + minimum_execution_duration?: number; } export interface AgentAction extends NewAgentAction { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx index a15692b718a32..64fa3ad96fed3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx @@ -82,11 +82,17 @@ describe('StepDefinePackagePolicy', () => { }; }); - const validationResults = { name: null, description: null, namespace: null, inputs: {} }; + const validationResults = { + name: null, + description: null, + namespace: null, + inputs: {}, + vars: {}, + }; let testRenderer: TestRenderer; let renderResult: ReturnType; - const render = () => + const render = ({ isUpdate } = { isUpdate: false }) => (renderResult = testRenderer.render( { updatePackagePolicy={mockUpdatePackagePolicy} validationResults={validationResults} submitAttempted={false} + isUpdate={isUpdate} /> )); @@ -199,4 +206,23 @@ describe('StepDefinePackagePolicy', () => { expect(renderResult.getByDisplayValue('apache-11')).toBeInTheDocument(); }); }); + + describe('update', () => { + describe('when package vars are introduced in a new package version', () => { + it('should display new package vars', () => { + render({ isUpdate: true }); + + waitFor(async () => { + expect(renderResult.getByDisplayValue('showUserVarVal')).toBeInTheDocument(); + expect(renderResult.getByText('Required var')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(renderResult.getByText('Advanced options').closest('button')!); + }); + + expect(renderResult.getByText('Advanced var')).toBeInTheDocument(); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 7f67452e2f230..893c68236aa6e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -90,9 +90,28 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ // Update package policy's package and agent policy info useEffect(() => { - if (isUpdate || isLoadingPackagePolicies) { + if (isLoadingPackagePolicies) { return; } + + if (isUpdate) { + // If we're upgrading, we need to make sure we catch an addition of package-level + // vars when they were previously no package-level vars defined + if (!packagePolicy.vars && packageInfo.vars) { + updatePackagePolicy( + packageToPackagePolicy( + packageInfo, + agentPolicy?.id || '', + packagePolicy.output_id, + packagePolicy.namespace, + packagePolicy.name, + packagePolicy.description, + integrationToEnable + ) + ); + } + } + const pkg = packagePolicy.package; const currentPkgKey = pkg ? pkgKeyFromPackageInfo(pkg) : ''; const pkgKey = pkgKeyFromPackageInfo(packageInfo); @@ -211,6 +230,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ const { name: varName, type: varType } = varDef; if (!packagePolicy.vars || !packagePolicy.vars[varName]) return null; const value = packagePolicy.vars[varName].value; + return ( 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/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx index 60a97845312e8..9f164d4aff13c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -15,6 +15,7 @@ import { EuiFlexItem, EuiPopover, EuiPortal, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -23,6 +24,8 @@ import type { Agent, AgentPolicy } from '../../../../types'; import { AgentEnrollmentFlyout, SearchBar } from '../../../../components'; import { AGENTS_INDEX } from '../../../../constants'; +import { MAX_TAG_DISPLAY_LENGTH, truncateTag } from '../utils'; + import { AgentBulkActions } from './bulk_actions'; import type { SelectionMode } from './types'; @@ -231,7 +234,13 @@ export const SearchAndFilterBar: React.FunctionComponent<{ } }} > - {tag} + {tag.length > MAX_TAG_DISPLAY_LENGTH ? ( + + {truncateTag(tag)} + + ) : ( + tag + )} ))}
    diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags.tsx index 7650b0d942180..9e084b07e64d1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags.tsx @@ -9,10 +9,13 @@ import { EuiToolTip } from '@elastic/eui'; import { take } from 'lodash'; import React from 'react'; +import { truncateTag } from '../utils'; + interface Props { tags: string[]; } +// Number of tags displayed before "+ N more" is displayed const MAX_TAGS_TO_DISPLAY = 3; export const Tags: React.FunctionComponent = ({ tags }) => { @@ -22,12 +25,12 @@ export const Tags: React.FunctionComponent = ({ tags }) => { <> {tags.join(', ')}}> - {take(tags, 3).join(', ')} + {tags.length - MAX_TAGS_TO_DISPLAY} more + {take(tags, 3).map(truncateTag).join(', ')} + {tags.length - MAX_TAGS_TO_DISPLAY} more ) : ( - {tags.join(', ')} + {tags.map(truncateTag).join(', ')} )} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 660a06911a5f0..be38f7688c735 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -144,6 +144,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { } if (selectedTags.length) { + if (kueryBuilder) { + kueryBuilder = `(${kueryBuilder}) and`; + } kueryBuilder = `${kueryBuilder} ${AGENTS_PREFIX}.tags : (${selectedTags .map((tag) => `"${tag}"`) .join(' or ')})`; @@ -338,6 +341,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.fleet.agentList.hostColumnTitle', { defaultMessage: 'Host', }), + width: '185px', render: (host: string, agent: Agent) => ( {safeMetadata(host)} @@ -346,7 +350,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, { field: 'active', - width: '120px', + width: '85px', name: i18n.translate('xpack.fleet.agentList.statusColumnTitle', { defaultMessage: 'Status', }), @@ -354,7 +358,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, { field: 'tags', - width: '240px', + width: '210px', name: i18n.translate('xpack.fleet.agentList.tagsColumnTitle', { defaultMessage: 'Tags', }), @@ -365,12 +369,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.fleet.agentList.policyColumnTitle', { defaultMessage: 'Agent policy', }), + width: '260px', render: (policyId: string, agent: Agent) => { const agentPolicy = agentPoliciesIndexedById[policyId]; const showWarning = agent.policy_revision && agentPolicy?.revision > agent.policy_revision; return ( - + {agentPolicy && } {showWarning && ( @@ -390,7 +395,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, { field: 'local_metadata.elastic.agent.version', - width: '200px', + width: '120px', name: i18n.translate('xpack.fleet.agentList.versionTitle', { defaultMessage: 'Version', }), @@ -419,6 +424,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.fleet.agentList.lastCheckinTitle', { defaultMessage: 'Last activity', }), + width: '180px', render: (lastCheckin: string, agent: any) => lastCheckin ? : null, }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/index.ts new file mode 100644 index 0000000000000..a549209ac6005 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/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 './truncate_tag'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/truncate_tag.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/truncate_tag.ts new file mode 100644 index 0000000000000..57046a4b284b9 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/truncate_tag.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. + */ + +// Number of characters to display for each tag before truncation +export const MAX_TAG_DISPLAY_LENGTH = 20; + +export function truncateTag(tag: string) { + return tag.length > MAX_TAG_DISPLAY_LENGTH + ? `${tag.substring(0, MAX_TAG_DISPLAY_LENGTH)}...` + : tag; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx index 9a95b9e834ec1..d76bb1ca7d035 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx @@ -80,12 +80,21 @@ export function validateLogstashHosts(value: string[]) { throw new Error('Invalid host'); } } catch (error) { - res.push({ - message: i18n.translate('xpack.fleet.settings.outputForm.logstashHostError', { - defaultMessage: 'Invalid Host', - }), - index: idx, - }); + if (val.length === 0) { + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.logstashHostRequiredError', { + defaultMessage: 'Host is required', + }), + index: idx, + }); + } else { + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.logstashHostError', { + defaultMessage: 'Invalid Host', + }), + index: idx, + }); + } } const curIndexes = urlIndexes[val] || []; 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/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index b42d2d4e0e5a4..eea3ac5c35f5f 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -19,6 +19,7 @@ import { appContextService } from '../services'; import { AgentNotFoundError, + AgentActionNotFoundError, AgentPolicyNameExistsError, ConcurrentInstallOperationError, IngestManagerError, @@ -65,6 +66,9 @@ const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof AgentNotFoundError) { return 404; } + if (error instanceof AgentActionNotFoundError) { + return 404; + } return 400; // Bad Request }; diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index cb1de48ad1958..1d1892f620e93 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -34,6 +34,7 @@ export class PackageOutdatedError extends IngestManagerError {} export class AgentPolicyError extends IngestManagerError {} export class AgentPolicyNotFoundError extends IngestManagerError {} export class AgentNotFoundError extends IngestManagerError {} +export class AgentActionNotFoundError extends IngestManagerError {} export class AgentPolicyNameExistsError extends AgentPolicyError {} export class PackageUnsupportedMediaTypeError extends IngestManagerError {} export class PackageInvalidArchiveError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts index 85f7ea672ecb4..7e7edaae70012 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts @@ -86,6 +86,7 @@ describe('test actions handlers', () => { id: 'agent', }), createAgentAction: jest.fn().mockReturnValueOnce(agentAction), + cancelAgentAction: jest.fn(), } as jest.Mocked; const postNewAgentActionHandler = postNewAgentActionHandlerBuilder(actionsService); diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index 80a6eac2d81b0..36c1fd8401584 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -10,7 +10,10 @@ import type { RequestHandler } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; -import type { PostNewAgentActionRequestSchema } from '../../types/rest_spec'; +import type { + PostNewAgentActionRequestSchema, + PostCancelActionRequestSchema, +} from '../../types/rest_spec'; import type { ActionsService } from '../../services/agents'; import type { PostNewAgentActionResponse } from '../../../common/types/rest_spec'; import { defaultIngestErrorHandler } from '../../errors'; @@ -46,3 +49,23 @@ export const postNewAgentActionHandlerBuilder = function ( } }; }; + +export const postCancelActionHandlerBuilder = function ( + actionsService: ActionsService +): RequestHandler, undefined, undefined> { + return async (context, request, response) => { + try { + const esClient = (await context.core).elasticsearch.client.asInternalUser; + + const action = await actionsService.cancelAgentAction(esClient, request.params.actionId); + + const body: PostNewAgentActionResponse = { + item: action, + }; + + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } + }; +}; diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 535bb780abe57..4f26f09944252 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -20,6 +20,7 @@ import { PostBulkAgentReassignRequestSchema, PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema, + PostCancelActionRequestSchema, } from '../../types'; import * as AgentService from '../../services/agents'; import type { FleetConfigType } from '../..'; @@ -35,7 +36,10 @@ import { postBulkAgentsReassignHandler, getAgentDataHandler, } from './handlers'; -import { postNewAgentActionHandlerBuilder } from './actions_handlers'; +import { + postNewAgentActionHandlerBuilder, + postCancelActionHandlerBuilder, +} from './actions_handlers'; import { postAgentUnenrollHandler, postBulkAgentsUnenrollHandler } from './unenroll_handler'; import { postAgentUpgradeHandler, postBulkAgentsUpgradeHandler } from './upgrade_handler'; @@ -96,6 +100,22 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT }, postNewAgentActionHandlerBuilder({ getAgent: AgentService.getAgentById, + cancelAgentAction: AgentService.cancelAgentAction, + createAgentAction: AgentService.createAgentAction, + }) + ); + + router.post( + { + path: AGENT_API_ROUTES.CANCEL_ACTIONS_PATTERN, + validate: PostCancelActionRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + postCancelActionHandlerBuilder({ + getAgent: AgentService.getAgentById, + cancelAgentAction: AgentService.cancelAgentAction, createAgentAction: AgentService.createAgentAction, }) ); diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 13df9222c9524..32c8276a9e5f8 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -7,15 +7,24 @@ import type { RequestHandler } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; +import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; + import semverCoerce from 'semver/functions/coerce'; +import semverGt from 'semver/functions/gt'; import type { PostAgentUpgradeResponse, PostBulkAgentUpgradeResponse } from '../../../common/types'; import type { PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema } from '../../types'; import * as AgentService from '../../services/agents'; import { appContextService } from '../../services'; import { defaultIngestErrorHandler } from '../../errors'; +import { SO_SEARCH_LIMIT } from '../../../common'; import { isAgentUpgradeable } from '../../../common/services'; -import { getAgentById } from '../../services/agents'; +import { getAgentById, getAgentsByKuery } from '../../services/agents'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, AGENTS_PREFIX } from '../../constants'; + +import { getMaxVersion } from '../../../common/services/get_max_version'; + +import { packagePolicyService } from '../../services/package_policy'; export const postAgentUpgradeHandler: RequestHandler< TypeOf, @@ -28,7 +37,7 @@ export const postAgentUpgradeHandler: RequestHandler< const { version, source_uri: sourceUri, force } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { - checkVersionIsSame(version, kibanaVersion); + checkKibanaVersion(version, kibanaVersion); checkSourceUriAllowed(sourceUri); } catch (err) { return response.customError({ @@ -81,11 +90,18 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< const coreContext = await context.core; const soClient = coreContext.savedObjects.client; const esClient = coreContext.elasticsearch.client.asInternalUser; - const { version, source_uri: sourceUri, agents, force } = request.body; + const { + version, + source_uri: sourceUri, + agents, + force, + rollout_duration_seconds: upgradeDurationSeconds, + } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { - checkVersionIsSame(version, kibanaVersion); + checkKibanaVersion(version, kibanaVersion); checkSourceUriAllowed(sourceUri); + await checkFleetServerVersion(version, agents, soClient, esClient); } catch (err) { return response.customError({ statusCode: 400, @@ -102,6 +118,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< sourceUri, version, force, + upgradeDurationSeconds, }; const results = await AgentService.sendUpgradeAgentsActions(soClient, esClient, upgradeOptions); const body = results.items.reduce((acc, so) => { @@ -118,17 +135,17 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< } }; -export const checkVersionIsSame = (version: string, kibanaVersion: string) => { +export const checkKibanaVersion = (version: string, kibanaVersion: string) => { // get version number only in case "-SNAPSHOT" is in it const kibanaVersionNumber = semverCoerce(kibanaVersion)?.version; if (!kibanaVersionNumber) throw new Error(`kibanaVersion ${kibanaVersionNumber} is not valid`); const versionToUpgradeNumber = semverCoerce(version)?.version; if (!versionToUpgradeNumber) throw new Error(`version to upgrade ${versionToUpgradeNumber} is not valid`); - // temporarily only allow upgrading to the same version as the installed kibana version - if (kibanaVersionNumber !== versionToUpgradeNumber) + + if (semverGt(version, kibanaVersion)) throw new Error( - `cannot upgrade agent to ${versionToUpgradeNumber} because it is different than the installed kibana version ${kibanaVersionNumber}` + `cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the installed kibana version ${kibanaVersionNumber}` ); }; @@ -139,3 +156,67 @@ const checkSourceUriAllowed = (sourceUri?: string) => { ); } }; + +// Check the installed fleet server versions +// Allow upgrading if the agents to upgrade include fleet server agents +const checkFleetServerVersion = async ( + versionToUpgradeNumber: string, + agentsIds: string | string[], + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient +) => { + let packagePolicyData; + try { + packagePolicyData = await packagePolicyService.list(soClient, { + perPage: SO_SEARCH_LIMIT, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: fleet_server`, + }); + } catch (error) { + throw new Error(error.message); + } + const agentPoliciesIds = packagePolicyData?.items.map((item) => item.policy_id); + + if (agentPoliciesIds.length === 0) { + return; + } + + let agentsResponse; + try { + agentsResponse = await getAgentsByKuery(esClient, { + showInactive: false, + perPage: SO_SEARCH_LIMIT, + kuery: `${AGENTS_PREFIX}.policy_id:${agentPoliciesIds.map((id) => `"${id}"`).join(' or ')}`, + }); + } catch (error) { + throw new Error(error.message); + } + + const { agents: fleetServerAgents } = agentsResponse; + + if (fleetServerAgents.length === 0) { + return; + } + const fleetServerIds = fleetServerAgents.map((agent) => agent.id); + + let hasFleetServerAgents: boolean; + if (Array.isArray(agentsIds)) { + hasFleetServerAgents = agentsIds.some((id) => fleetServerIds.includes(id)); + } else { + hasFleetServerAgents = fleetServerIds.includes(agentsIds); + } + if (hasFleetServerAgents) { + return; + } + + const fleetServerVersions = fleetServerAgents.map( + (agent) => agent.local_metadata.elastic.agent.version + ) as string[]; + + const maxFleetServerVersion = getMaxVersion(fleetServerVersions); + + if (semverGt(versionToUpgradeNumber, maxFleetServerVersion)) { + throw new Error( + `cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the latest fleet server version ${maxFleetServerVersion}` + ); + } +}; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts new file mode 100644 index 0000000000000..2838f2204ad96 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; + +import { cancelAgentAction } from './actions'; + +describe('Agent actions', () => { + describe('cancelAgentAction', () => { + it('throw if the target action is not found', async () => { + const esClient = elasticsearchServiceMock.createInternalClient(); + esClient.search.mockResolvedValue({ + hits: { + hits: [], + }, + } as any); + await expect(() => cancelAgentAction(esClient, 'i-do-not-exists')).rejects.toThrowError( + /Action not found/ + ); + }); + + it('should create one CANCEL action for each action found', async () => { + const esClient = elasticsearchServiceMock.createInternalClient(); + esClient.search.mockResolvedValue({ + hits: { + hits: [ + { + _source: { + action_id: 'action1', + agents: ['agent1', 'agent2'], + expiration: '2022-05-12T18:16:18.019Z', + }, + }, + { + _source: { + action_id: 'action1', + agents: ['agent3', 'agent4'], + expiration: '2022-05-12T18:16:18.019Z', + }, + }, + ], + }, + } as any); + await cancelAgentAction(esClient, 'action1'); + + expect(esClient.create).toBeCalledTimes(2); + expect(esClient.create).toBeCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + type: 'CANCEL', + data: { target_id: 'action1' }, + agents: ['agent1', 'agent2'], + }), + }) + ); + expect(esClient.create).toBeCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + type: 'CANCEL', + data: { target_id: 'action1' }, + agents: ['agent3', 'agent4'], + }), + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index 7a13e1612cb0c..afa65bfe91fb3 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -14,7 +14,8 @@ import type { NewAgentAction, FleetServerAgentAction, } from '../../../common/types/models'; -import { AGENT_ACTIONS_INDEX } from '../../../common/constants'; +import { AGENT_ACTIONS_INDEX, SO_SEARCH_LIMIT } from '../../../common/constants'; +import { AgentActionNotFoundError } from '../../errors'; const ONE_MONTH_IN_MS = 2592000000; @@ -22,26 +23,28 @@ export async function createAgentAction( esClient: ElasticsearchClient, newAgentAction: NewAgentAction ): Promise { - const id = newAgentAction.id ?? uuid.v4(); + const actionId = newAgentAction.id ?? uuid.v4(); const timestamp = new Date().toISOString(); const body: FleetServerAgentAction = { '@timestamp': timestamp, - expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + expiration: newAgentAction.expiration ?? new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), agents: newAgentAction.agents, - action_id: id, + action_id: actionId, data: newAgentAction.data, type: newAgentAction.type, + start_time: newAgentAction.start_time, + minimum_execution_duration: newAgentAction.minimum_execution_duration, }; await esClient.create({ index: AGENT_ACTIONS_INDEX, - id, + id: uuid.v4(), body, refresh: 'wait_for', }); return { - id, + id: actionId, ...newAgentAction, created_at: timestamp, }; @@ -49,18 +52,18 @@ export async function createAgentAction( export async function bulkCreateAgentActions( esClient: ElasticsearchClient, - newAgentActions: Array> + newAgentActions: NewAgentAction[] ): Promise { const actions = newAgentActions.map((newAgentAction) => { - const id = uuid.v4(); + const id = newAgentAction.id ?? uuid.v4(); return { id, ...newAgentAction, - }; + } as AgentAction; }); if (actions.length === 0) { - return actions; + return []; } await esClient.bulk({ @@ -68,7 +71,9 @@ export async function bulkCreateAgentActions( body: actions.flatMap((action) => { const body: FleetServerAgentAction = { '@timestamp': new Date().toISOString(), - expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + expiration: action.expiration ?? new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + start_time: action.start_time, + minimum_execution_duration: action.minimum_execution_duration, agents: action.agents, action_id: action.id, data: action.data, @@ -89,9 +94,57 @@ export async function bulkCreateAgentActions( return actions; } +export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: string) { + const res = await esClient.search({ + index: AGENT_ACTIONS_INDEX, + query: { + bool: { + must: [ + { + term: { + action_id: actionId, + }, + }, + ], + }, + }, + size: SO_SEARCH_LIMIT, + }); + + if (res.hits.hits.length === 0) { + throw new AgentActionNotFoundError('Action not found'); + } + + const cancelActionId = uuid.v4(); + const now = new Date().toISOString(); + for (const hit of res.hits.hits) { + if (!hit._source || !hit._source.agents || !hit._source.action_id) { + continue; + } + await createAgentAction(esClient, { + id: cancelActionId, + type: 'CANCEL', + agents: hit._source.agents, + data: { + target_id: hit._source.action_id, + }, + created_at: now, + expiration: hit._source.expiration, + }); + } + + return { + created_at: now, + id: cancelActionId, + type: 'CANCEL', + } as AgentAction; +} + export interface ActionsService { getAgent: (esClient: ElasticsearchClient, agentId: string) => Promise; + cancelAgentAction: (esClient: ElasticsearchClient, actionId: string) => Promise; + createAgentAction: ( esClient: ElasticsearchClient, newAgentAction: Omit diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 00470d5e25f8d..f1bd60d1eba94 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -6,6 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import moment from 'moment'; import type { Agent, BulkActionResult } from '../../types'; import { agentPolicyService } from '..'; @@ -28,6 +29,8 @@ import { } from './crud'; import { searchHitToAgent } from './helpers'; +const MINIMUM_EXECUTION_DURATION_SECONDS = 1800; // 30m + function isMgetDoc(doc?: estypes.MgetResponseItem): doc is estypes.GetGetResult { return Boolean(doc && 'found' in doc); } @@ -78,6 +81,7 @@ export async function sendUpgradeAgentsActions( version: string; sourceUri?: string | undefined; force?: boolean; + upgradeDurationSeconds?: number; } ) { // Full set of agents @@ -158,12 +162,21 @@ export async function sendUpgradeAgentsActions( source_uri: options.sourceUri, }; + const rollingUpgradeOptions = options?.upgradeDurationSeconds + ? { + start_time: now, + minimum_execution_duration: MINIMUM_EXECUTION_DURATION_SECONDS, + expiration: moment().add(options?.upgradeDurationSeconds, 'seconds').toISOString(), + } + : {}; + await createAgentAction(esClient, { created_at: now, data, ack_data: data, type: 'UPGRADE', agents: agentsToUpdate.map((agent) => agent.id), + ...rollingUpgradeOptions, }); await bulkUpdateAgents( diff --git a/x-pack/plugins/fleet/server/telemetry/sender.ts b/x-pack/plugins/fleet/server/telemetry/sender.ts index a2f9bcafbcbb8..8c53fc850741f 100644 --- a/x-pack/plugins/fleet/server/telemetry/sender.ts +++ b/x-pack/plugins/fleet/server/telemetry/sender.ts @@ -168,7 +168,7 @@ export class TelemetryEventsSender { const resp = await axios.post(telemetryUrl, ndjson, { headers: { 'Content-Type': 'application/x-ndjson', - 'X-Elastic-Cluster-ID': clusterUuid, + ...(clusterUuid ? { 'X-Elastic-Cluster-ID': clusterUuid } : undefined), 'X-Elastic-Stack-Version': clusterVersionNumber ? clusterVersionNumber : '7.16.0', }, }); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index ea11637119dc9..e080fe66f7e2c 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -34,6 +34,12 @@ export const PostNewAgentActionRequestSchema = { }), }; +export const PostCancelActionRequestSchema = { + params: schema.object({ + actionId: schema.string(), + }), +}; + export const PostAgentUnenrollRequestSchema = { params: schema.object({ agentId: schema.string(), @@ -71,6 +77,7 @@ export const PostBulkAgentUpgradeRequestSchema = { source_uri: schema.maybe(schema.string()), version: schema.string(), force: schema.maybe(schema.boolean()), + rollout_duration_seconds: schema.maybe(schema.number({ min: 600 })), }), }; 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/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index a28686595053b..febade31fa4aa 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -118,10 +118,10 @@ export const BuilderEntryItem: React.FC = ({ const handleOperatorChange = useCallback( ([newOperator]: OperatorOption[]): void => { const { updatedEntry, index } = getEntryOnOperatorChange(entry, newOperator); - + handleError(false); onChange(updatedEntry, index); }, - [onChange, entry] + [onChange, entry, handleError] ); const handleFieldMatchValueChange = useCallback( diff --git a/x-pack/plugins/maps/common/execution_context.ts b/x-pack/plugins/maps/common/execution_context.ts index 4a11eb5d89029..f62f1da85f99d 100644 --- a/x-pack/plugins/maps/common/execution_context.ts +++ b/x-pack/plugins/maps/common/execution_context.ts @@ -6,9 +6,14 @@ */ import { isUndefined, omitBy } from 'lodash'; +import type { KibanaExecutionContext } from '@kbn/core/public'; import { APP_ID } from './constants'; -export function makeExecutionContext(context: { id?: string; url?: string; description?: string }) { +export function makeExecutionContext(context: { + id?: string; + url?: string; + description?: string; +}): KibanaExecutionContext { return omitBy( { name: APP_ID, diff --git a/x-pack/plugins/maps/common/mvt_request_body.ts b/x-pack/plugins/maps/common/mvt_request_body.ts index 16f9d2cce6381..e5517b23e0cba 100644 --- a/x-pack/plugins/maps/common/mvt_request_body.ts +++ b/x-pack/plugins/maps/common/mvt_request_body.ts @@ -7,6 +7,7 @@ import type { RisonValue } from 'rison-node'; import rison from 'rison-node'; +import { RENDER_AS } from './constants'; export function decodeMvtResponseBody(encodedRequestBody: string): object { return rison.decode(decodeURIComponent(encodedRequestBody)) as object; @@ -15,3 +16,107 @@ export function decodeMvtResponseBody(encodedRequestBody: string): object { export function encodeMvtResponseBody(unencodedRequestBody: object): string { return encodeURIComponent(rison.encode(unencodedRequestBody as RisonValue)); } + +export function getAggsTileRequest({ + encodedRequestBody, + geometryFieldName, + gridPrecision, + index, + renderAs = RENDER_AS.POINT, + x, + y, + z, +}: { + encodedRequestBody: string; + geometryFieldName: string; + gridPrecision: number; + index: string; + renderAs: RENDER_AS; + x: number; + y: number; + z: number; +}) { + const requestBody = decodeMvtResponseBody(encodedRequestBody) as any; + return { + path: `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`, + body: { + size: 0, // no hits + grid_precision: gridPrecision, + exact_bounds: false, + extent: 4096, // full resolution, + query: requestBody.query, + grid_agg: renderAs === RENDER_AS.HEX ? 'geohex' : 'geotile', + grid_type: renderAs === RENDER_AS.GRID || renderAs === RENDER_AS.HEX ? 'grid' : 'centroid', + aggs: requestBody.aggs, + fields: requestBody.fields, + runtime_mappings: requestBody.runtime_mappings, + }, + }; +} + +export function getHitsTileRequest({ + encodedRequestBody, + geometryFieldName, + index, + x, + y, + z, +}: { + encodedRequestBody: string; + geometryFieldName: string; + index: string; + x: number; + y: number; + z: number; +}) { + const requestBody = decodeMvtResponseBody(encodedRequestBody) as any; + return { + path: `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`, + body: { + grid_precision: 0, // no aggs + exact_bounds: true, + extent: 4096, // full resolution, + query: requestBody.query, + fields: mergeFields( + [ + requestBody.docvalue_fields as Field[] | undefined, + requestBody.stored_fields as Field[] | undefined, + ], + [geometryFieldName] + ), + runtime_mappings: requestBody.runtime_mappings, + track_total_hits: typeof requestBody.size === 'number' ? requestBody.size + 1 : false, + }, + }; +} + +// can not use "import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey" +// SearchRequest is incorrectly typed and does not support Field as object +// https://github.com/elastic/elasticsearch-js/issues/1615 +type Field = + | string + | { + field: string; + format: string; + }; + +function mergeFields(fieldsList: Array, excludeNames: string[]): Field[] { + const fieldNames: string[] = []; + const mergedFields: Field[] = []; + + fieldsList.forEach((fields) => { + if (!fields) { + return; + } + + fields.forEach((field) => { + const fieldName = typeof field === 'string' ? field : field.field; + if (!excludeNames.includes(fieldName) && !fieldNames.includes(fieldName)) { + fieldNames.push(fieldName); + mergedFields.push(field); + } + }); + }); + + return mergedFields; +} 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/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 25ba0ac862db8..5a1c37c11b80d 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -22,7 +22,7 @@ import { getSelectedLayerId, } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; -import { cancelRequest } from '../reducers/non_serializable_instances'; +import { cancelRequest, getInspectorAdapters } from '../reducers/non_serializable_instances'; import { setDrawMode, updateFlyout } from './ui_actions'; import { ADD_LAYER, @@ -451,6 +451,9 @@ function updateLayerType(layerId: string, newLayerType: string) { return; } dispatch(clearDataRequests(layer)); + if (layer.getSource().isESSource()) { + getInspectorAdapters(getState()).vectorTiles?.removeLayer(layerId); + } dispatch({ type: UPDATE_LAYER_PROP, id: layerId, @@ -587,6 +590,9 @@ function removeLayerFromLayerList(layerId: string) { }); dispatch(updateTooltipStateForLayer(layerGettingRemoved)); layerGettingRemoved.destroy(); + if (layerGettingRemoved.getSource().isESSource()) { + getInspectorAdapters(getState())?.vectorTiles.removeLayer(layerId); + } dispatch({ type: REMOVE_LAYER, id: layerId, diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.ts b/x-pack/plugins/maps/public/actions/map_actions.test.ts index 935ca332baa22..407b7ef48ea52 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.test.ts @@ -8,6 +8,7 @@ /* eslint @typescript-eslint/no-var-requires: 0 */ jest.mock('../selectors/map_selectors', () => ({})); +jest.mock('../reducers/non_serializable_instances', () => ({})); jest.mock('./data_request_actions', () => { return { syncDataForAllLayers: () => {}, @@ -25,6 +26,9 @@ import { mapExtentChanged, setMouseCoordinates, setQuery } from './map_actions'; const getStoreMock = jest.fn(); const dispatchMock = jest.fn(); +const vectorTileAdapterMock = { + setTiles: jest.fn(), +}; describe('map_actions', () => { afterEach(() => { @@ -43,6 +47,12 @@ describe('map_actions', () => { require('../selectors/map_selectors').getLayerList = () => { return []; }; + + require('../reducers/non_serializable_instances').getInspectorAdapters = () => { + return { + vectorTiles: vectorTileAdapterMock, + }; + }; }); it('should set buffer', () => { @@ -61,6 +71,8 @@ describe('map_actions', () => { }); action(dispatchMock, getStoreMock); + expect(vectorTileAdapterMock.setTiles.mock.calls[0]).toEqual([[{ x: 24, y: 15, z: 5 }]]); + expect(dispatchMock.mock.calls[0]).toEqual([ { mapViewContext: { @@ -101,6 +113,12 @@ describe('map_actions', () => { minLon: 92.5, }, }; + + require('../reducers/non_serializable_instances').getInspectorAdapters = () => { + return { + vectorTiles: vectorTileAdapterMock, + }; + }; }; }); @@ -120,6 +138,8 @@ describe('map_actions', () => { }); action(dispatchMock, getStoreMock); + expect(vectorTileAdapterMock.setTiles.mock.calls.length).toBe(0); + expect(dispatchMock.mock.calls[0]).toEqual([ { mapViewContext: { @@ -162,6 +182,8 @@ describe('map_actions', () => { }); action(dispatchMock, getStoreMock); + expect(vectorTileAdapterMock.setTiles.mock.calls.length).toBe(1); + expect(dispatchMock.mock.calls[0]).toEqual([ { mapViewContext: { @@ -204,6 +226,8 @@ describe('map_actions', () => { }); action(dispatchMock, getStoreMock); + expect(vectorTileAdapterMock.setTiles.mock.calls.length).toBe(1); + expect(dispatchMock.mock.calls[0]).toEqual([ { mapViewContext: { diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index e8585560238fd..c23b77326f293 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -17,6 +17,7 @@ import { Geometry, Position } from 'geojson'; import { asyncForEach, asyncMap } from '@kbn/std'; import { DRAW_MODE, DRAW_SHAPE, LAYER_STYLE_TYPE } from '../../common/constants'; import type { MapExtentState, MapViewContext } from '../reducers/map/types'; +import { getInspectorAdapters } from '../reducers/non_serializable_instances'; import { MapStoreState } from '../reducers/store'; import { IVectorStyle } from '../classes/styles/vector/vector_style'; import { @@ -73,7 +74,7 @@ import { INITIAL_LOCATION } from '../../common/constants'; import { updateTooltipStateForLayer } from './tooltip_actions'; import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE, pushDeletedFeatureId, clearDeletedFeatureIds } from './ui_actions'; -import { expandToTileBoundaries } from '../classes/util/geo_tile_utils'; +import { expandToTileBoundaries, getTilesForExtent } from '../classes/util/geo_tile_utils'; import { getToasts } from '../kibana_services'; import { getDeletedFeatureIds } from '../selectors/ui_selectors'; @@ -217,14 +218,18 @@ export function mapExtentChanged(mapExtentState: MapExtentState) { doesPrevBufferContainNextExtent = turfBooleanContains(bufferGeometry, extentGeometry); } + const requiresNewBuffer = + !prevBuffer || !doesPrevBufferContainNextExtent || prevZoom !== nextZoom; + if (requiresNewBuffer) { + getInspectorAdapters(getState()).vectorTiles.setTiles(getTilesForExtent(nextZoom, extent)); + } dispatch({ type: MAP_EXTENT_CHANGED, mapViewContext: { ...mapExtentState, - buffer: - !prevBuffer || !doesPrevBufferContainNextExtent || prevZoom !== nextZoom - ? expandToTileBoundaries(extent, Math.ceil(nextZoom)) - : prevBuffer, + buffer: requiresNewBuffer + ? expandToTileBoundaries(extent, Math.ceil(nextZoom)) + : prevBuffer, } as MapViewContext, }); diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index d1247590af2e9..0906e39ed37fc 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -94,6 +94,7 @@ export class HeatmapLayer extends AbstractLayer { async syncData(syncContext: DataRequestContext) { await syncMvtSourceData({ layerId: this.getId(), + layerName: await this.getDisplayName(), prevDataRequest: this.getSourceDataRequest(), requestMeta: buildVectorRequestMeta( this.getSource(), 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/mvt_source_data.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts index ccc73f94aac57..735d38f0f3624 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts @@ -42,6 +42,9 @@ const mockSource = { isGeoGridPrecisionAware: () => { return false; }, + isESSource: () => { + return false; + }, } as unknown as IMvtVectorSource; describe('syncMvtSourceData', () => { @@ -50,6 +53,7 @@ describe('syncMvtSourceData', () => { await syncMvtSourceData({ layerId: 'layer1', + layerName: 'my layer', prevDataRequest: undefined, requestMeta: { ...syncContext.dataFilters, @@ -96,6 +100,7 @@ describe('syncMvtSourceData', () => { await syncMvtSourceData({ layerId: 'layer1', + layerName: 'my layer', prevDataRequest: { getMeta: () => { return prevRequestMeta; @@ -138,6 +143,7 @@ describe('syncMvtSourceData', () => { await syncMvtSourceData({ layerId: 'layer1', + layerName: 'my layer', prevDataRequest: { getMeta: () => { return prevRequestMeta; @@ -177,6 +183,7 @@ describe('syncMvtSourceData', () => { await syncMvtSourceData({ layerId: 'layer1', + layerName: 'my layer', prevDataRequest: { getMeta: () => { return prevRequestMeta; @@ -224,6 +231,7 @@ describe('syncMvtSourceData', () => { await syncMvtSourceData({ layerId: 'layer1', + layerName: 'my layer', prevDataRequest: { getMeta: () => { return prevRequestMeta; @@ -263,6 +271,7 @@ describe('syncMvtSourceData', () => { await syncMvtSourceData({ layerId: 'layer1', + layerName: 'my layer', prevDataRequest: { getMeta: () => { return prevRequestMeta; @@ -302,6 +311,7 @@ describe('syncMvtSourceData', () => { await syncMvtSourceData({ layerId: 'layer1', + layerName: 'my layer', prevDataRequest: { getMeta: () => { return prevRequestMeta; @@ -325,4 +335,38 @@ describe('syncMvtSourceData', () => { // @ts-expect-error sinon.assert.calledOnce(syncContext.stopLoading); }); + + test('Should add layer to vector tile inspector when source is synced', async () => { + const syncContext = new MockSyncContext({ dataFilters: {} }); + const mockVectorTileAdapter = { + addLayer: sinon.spy(), + }; + + await syncMvtSourceData({ + layerId: 'layer1', + layerName: 'my layer', + prevDataRequest: undefined, + requestMeta: { + ...syncContext.dataFilters, + applyGlobalQuery: true, + applyGlobalTime: true, + applyForceRefresh: true, + fieldNames: [], + sourceMeta: {}, + isForceRefresh: false, + isFeatureEditorOpenForLayer: false, + }, + source: { + ...mockSource, + isESSource: () => { + return true; + }, + getInspectorAdapters: () => { + return { vectorTiles: mockVectorTileAdapter }; + }, + }, + syncContext, + }); + sinon.assert.calledOnce(mockVectorTileAdapter.addLayer); + }); }); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts index f20ab0b5d200f..daceeac1f072e 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts @@ -24,12 +24,14 @@ export interface MvtSourceData { export async function syncMvtSourceData({ layerId, + layerName, prevDataRequest, requestMeta, source, syncContext, }: { layerId: string; + layerName: string; prevDataRequest: DataRequest | undefined; requestMeta: VectorSourceRequestMeta; source: IMvtVectorSource; @@ -71,6 +73,9 @@ export async function syncMvtSourceData({ : prevData.refreshToken; const tileUrl = await source.getTileUrl(requestMeta, refreshToken); + if (source.isESSource()) { + source.getInspectorAdapters()?.vectorTiles.addLayer(layerId, layerName, tileUrl); + } const sourceData = { tileUrl, tileSourceLayer: source.getTileSourceLayer(), diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index adb211f8f9420..462ea5b0cc8f1 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -220,6 +220,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { await syncMvtSourceData({ layerId: this.getId(), + layerName: await this.getDisplayName(), prevDataRequest: this.getSourceDataRequest(), requestMeta: await this._getVectorSourceRequestMeta( syncContext.isForceRefresh, 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/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap index 513e3d4148efd..f8c5951e95e04 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap @@ -11,6 +11,7 @@ exports[`scaling form should disable clusters option when clustering is not supp id="xpack.maps.esSearch.scaleTitle" values={Object {}} /> + + { {this._renderModal()}
    - + {' '} { +test('Should parse tile key', () => { expect(parseTileKey('15/23423/1867')).toEqual({ zoom: 15, x: 23423, @@ -21,11 +22,76 @@ it('Should parse tile key', () => { }); }); -it('Should get tile key', () => { +test('Should get tiles for extent', () => { + const extent = { + minLon: -132.19235, + minLat: 12.05834, + maxLon: -83.6593, + maxLat: 30.03121, + }; + + expect(getTilesForExtent(4.74, extent)).toEqual([ + { x: 2, y: 6, z: 4 }, + { x: 2, y: 7, z: 4 }, + { x: 3, y: 6, z: 4 }, + { x: 3, y: 7, z: 4 }, + { x: 4, y: 6, z: 4 }, + { x: 4, y: 7, z: 4 }, + ]); +}); + +test('Should get tiles for extent that crosses dateline', () => { + const extent = { + minLon: -267.34624, + minLat: 10, + maxLon: 33.8355, + maxLat: 79.16772, + }; + + expect(getTilesForExtent(2.12, extent)).toEqual([ + { x: 3, y: 0, z: 2 }, + { x: 3, y: 1, z: 2 }, + { x: 0, y: 0, z: 2 }, + { x: 0, y: 1, z: 2 }, + { x: 1, y: 0, z: 2 }, + { x: 1, y: 1, z: 2 }, + { x: 2, y: 0, z: 2 }, + { x: 2, y: 1, z: 2 }, + ]); +}); + +test('Should get tiles for extent that crosses dateline and not add tiles in between right and left', () => { + const extent = { + minLon: -183.25917, + minLat: 50.10446, + maxLon: -176.63722, + maxLat: 53.06071, + }; + + expect(getTilesForExtent(6.8, extent)).toEqual([ + { x: 63, y: 20, z: 6 }, + { x: 63, y: 21, z: 6 }, + { x: 0, y: 20, z: 6 }, + { x: 0, y: 21, z: 6 }, + ]); +}); + +test('Should return single tile for zoom level 0', () => { + const extent = { + minLon: -180.39426, + minLat: -85.05113, + maxLon: 270.66456, + maxLat: 85.05113, + }; + + expect(getTilesForExtent(0, extent)).toEqual([{ x: 0, y: 0, z: 0 }]); +}); + +test('Should get tile key', () => { expect(getTileKey(45, 120, 10)).toEqual('10/853/368'); }); -it('Should convert tile key to geojson Polygon', () => { +test('Should convert tile key to geojson Polygon', () => { const geometry = getTileBoundingBox('15/23423/1867'); expect(geometry).toEqual({ top: 82.92546, @@ -35,7 +101,7 @@ it('Should convert tile key to geojson Polygon', () => { }); }); -it('Should convert tile key to geojson Polygon with extra precision', () => { +test('Should convert tile key to geojson Polygon with extra precision', () => { const geometry = getTileBoundingBox('26/19762828/25222702'); expect(geometry).toEqual({ top: 40.7491508, @@ -45,7 +111,7 @@ it('Should convert tile key to geojson Polygon with extra precision', () => { }); }); -it('Should expand extent to align boundaries with tile boundaries', () => { +test('Should expand extent to align boundaries with tile boundaries', () => { const extent = { maxLat: 12.5, maxLon: 102.5, diff --git a/x-pack/plugins/maps/public/classes/util/geo_tile_utils.ts b/x-pack/plugins/maps/public/classes/util/geo_tile_utils.ts index f3fe55b5e47c6..32343e7275841 100644 --- a/x-pack/plugins/maps/public/classes/util/geo_tile_utils.ts +++ b/x-pack/plugins/maps/public/classes/util/geo_tile_utils.ts @@ -60,6 +60,31 @@ export function parseTileKey(tileKey: string): { return { x, y, zoom, tileCount }; } +export function getTilesForExtent( + zoom: number, + extent: MapExtent +): Array<{ x: number; y: number; z: number }> { + const tileCount = getTileCount(Math.floor(zoom)); + const minX = longitudeToTile(extent.minLon, tileCount); + const maxX = longitudeToTile(extent.maxLon, tileCount); + const minY = latitudeToTile(extent.maxLat, tileCount); + const maxY = latitudeToTile(extent.minLat, tileCount); + + const tiles: Array<{ x: number; y: number; z: number }> = []; + for (let x = 0; x < tileCount && minX + x <= maxX; x++) { + const tileX = minX + x; + for (let y = 0; y < tileCount && minY + y <= maxY; y++) { + const tileY = minY + y; + tiles.push({ + x: tileX < 0 ? tileCount - Math.abs(tileX) : tileX, + y: tileY < 0 ? tileCount - Math.abs(tileY) : tileY, + z: Math.floor(zoom), + }); + } + } + return tiles; +} + export function getTileKey(lat: number, lon: number, zoom: number): string { const tileCount = getTileCount(zoom); diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap index 7daacae707ecb..de9d74f68f965 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap @@ -6,18 +6,13 @@ exports[`Should render callout when joins are disabled 1`] = ` size="xs" >
    - - - + + +
    - - - + + +
    - - - + {' '} +
    diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_documentation_popover.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_documentation_popover.tsx new file mode 100644 index 0000000000000..1799b7264611d --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_documentation_popover.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { EuiButtonIcon, EuiLink, EuiPopover, EuiPopoverTitle, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { getDocLinks } from '../../../../kibana_services'; + +interface State { + isPopoverOpen: boolean; +} + +export class JoinDocumentationPopover extends Component<{}, State> { + state: State = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + _renderContent() { + return ( +
    + +

    + +

    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    + +

    + + + +
    +
    + ); + } + + render() { + return ( + + } + isOpen={this.state.isPopoverOpen} + closePopover={this._closePopover} + repositionOnScroll + ownFocus + > + + + + {this._renderContent()} + + ); + } +} 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/inspector/index.ts b/x-pack/plugins/maps/public/inspector/index.ts new file mode 100644 index 0000000000000..149e5150d641f --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/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 { MapAdapter, MapInspectorView } from './map_adapter'; +export { VectorTileAdapter, VectorTileInspectorView } from './vector_tile_adapter'; diff --git a/x-pack/plugins/maps/public/inspector/map_adapter/index.ts b/x-pack/plugins/maps/public/inspector/map_adapter/index.ts new file mode 100644 index 0000000000000..397906aa563e4 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/map_adapter/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 { MapAdapter } from './map_adapter'; +export { MapInspectorView } from './map_inspector_view'; diff --git a/x-pack/plugins/maps/public/inspector/map_adapter.ts b/x-pack/plugins/maps/public/inspector/map_adapter/map_adapter.ts similarity index 100% rename from x-pack/plugins/maps/public/inspector/map_adapter.ts rename to x-pack/plugins/maps/public/inspector/map_adapter/map_adapter.ts diff --git a/x-pack/plugins/maps/public/inspector/map_details.tsx b/x-pack/plugins/maps/public/inspector/map_adapter/map_details.tsx similarity index 100% rename from x-pack/plugins/maps/public/inspector/map_details.tsx rename to x-pack/plugins/maps/public/inspector/map_adapter/map_details.tsx diff --git a/x-pack/plugins/maps/public/inspector/map_inspector_view.tsx b/x-pack/plugins/maps/public/inspector/map_adapter/map_inspector_view.tsx similarity index 95% rename from x-pack/plugins/maps/public/inspector/map_inspector_view.tsx rename to x-pack/plugins/maps/public/inspector/map_adapter/map_inspector_view.tsx index 7f65a630b72bd..d320dc4e9ed1c 100644 --- a/x-pack/plugins/maps/public/inspector/map_inspector_view.tsx +++ b/x-pack/plugins/maps/public/inspector/map_adapter/map_inspector_view.tsx @@ -8,7 +8,7 @@ import React, { lazy } from 'react'; import type { Adapters } from '@kbn/inspector-plugin/public'; import { i18n } from '@kbn/i18n'; -import { LazyWrapper } from '../lazy_wrapper'; +import { LazyWrapper } from '../../lazy_wrapper'; const getLazyComponent = () => { return lazy(() => import('./map_view_component')); diff --git a/x-pack/plugins/maps/public/inspector/map_view_component.tsx b/x-pack/plugins/maps/public/inspector/map_adapter/map_view_component.tsx similarity index 100% rename from x-pack/plugins/maps/public/inspector/map_view_component.tsx rename to x-pack/plugins/maps/public/inspector/map_adapter/map_view_component.tsx diff --git a/x-pack/plugins/maps/public/inspector/types.ts b/x-pack/plugins/maps/public/inspector/map_adapter/types.ts similarity index 100% rename from x-pack/plugins/maps/public/inspector/types.ts rename to x-pack/plugins/maps/public/inspector/map_adapter/types.ts diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/empty_prompt.tsx b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/empty_prompt.tsx new file mode 100644 index 0000000000000..db9a905a86afa --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/empty_prompt.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +export function EmptyPrompt() { + return ( + + + + } + body={ + +

    + +

    +
    + } + /> + ); +} diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.test.ts b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.test.ts new file mode 100644 index 0000000000000..a45be3cf80ec0 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getTileRequest } from './get_tile_request'; + +test('Should return elasticsearch vector tile request for aggs tiles', () => { + expect( + getTileRequest({ + layerId: '1', + tileUrl: `/pof/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=kibana_sample_data_logs&gridPrecision=8&requestBody=(_source%3A(excludes%3A!())%2Caggs%3A()%2Cfields%3A!((field%3A'%40timestamp'%2Cformat%3Adate_time)%2C(field%3Atimestamp%2Cformat%3Adate_time)%2C(field%3Autc_time%2Cformat%3Adate_time))%2Cquery%3A(bool%3A(filter%3A!((match_phrase%3A(machine.os.keyword%3Aios))%2C(range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A'2022-04-22T16%3A46%3A00.744Z'%2Clte%3A'2022-04-29T16%3A46%3A05.345Z'))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A'emit(doc%5B!'timestamp!'%5D.value.getHour())%3B')%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A0%2Cstored_fields%3A!('*'))&renderAs=heatmap&token=e8bff005-ccea-464a-ae56-2061b4f8ce68`, + x: 3, + y: 0, + z: 2, + }) + ).toEqual({ + path: '/kibana_sample_data_logs/_mvt/geo.coordinates/2/3/0', + body: { + size: 0, + grid_precision: 8, + exact_bounds: false, + extent: 4096, + query: { + bool: { + filter: [ + { + match_phrase: { + 'machine.os.keyword': 'ios', + }, + }, + { + range: { + timestamp: { + format: 'strict_date_optional_time', + gte: '2022-04-22T16:46:00.744Z', + lte: '2022-04-29T16:46:05.345Z', + }, + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }, + grid_agg: 'geotile', + grid_type: 'centroid', + aggs: {}, + fields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'timestamp', + format: 'date_time', + }, + { + field: 'utc_time', + format: 'date_time', + }, + ], + runtime_mappings: { + hour_of_day: { + script: { + source: "emit(doc['timestamp'].value.getHour());", + }, + type: 'long', + }, + }, + }, + }); +}); + +test('Should return elasticsearch vector tile request for hits tiles', () => { + expect( + getTileRequest({ + layerId: '1', + tileUrl: `http://localhost:5601/pof/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=kibana_sample_data_logs&requestBody=(_source%3A!f%2Cdocvalue_fields%3A!()%2Cquery%3A(bool%3A(filter%3A!((range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A%272022-04-22T16%3A46%3A00.744Z%27%2Clte%3A%272022-04-29T16%3A46%3A05.345Z%27))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A%27emit(doc%5B!%27timestamp!%27%5D.value.getHour())%3B%27)%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A10000%2Cstored_fields%3A!(geo.coordinates))&token=415049b6-bb0a-444a-a7b9-89717db5183c`, + x: 0, + y: 0, + z: 2, + }) + ).toEqual({ + path: '/kibana_sample_data_logs/_mvt/geo.coordinates/2/0/0', + body: { + grid_precision: 0, + exact_bounds: true, + extent: 4096, + query: { + bool: { + filter: [ + { + range: { + timestamp: { + format: 'strict_date_optional_time', + gte: '2022-04-22T16:46:00.744Z', + lte: '2022-04-29T16:46:05.345Z', + }, + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }, + fields: [], + runtime_mappings: { + hour_of_day: { + script: { + source: "emit(doc['timestamp'].value.getHour());", + }, + type: 'long', + }, + }, + track_total_hits: 10001, + }, + }); +}); diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.ts b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.ts new file mode 100644 index 0000000000000..f483dfda23409 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/get_tile_request.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + MVT_GETGRIDTILE_API_PATH, + MVT_GETTILE_API_PATH, + RENDER_AS, +} from '../../../../common/constants'; +import { getAggsTileRequest, getHitsTileRequest } from '../../../../common/mvt_request_body'; +import type { TileRequest } from '../types'; + +function getSearchParams(url: string): URLSearchParams { + const split = url.split('?'); + const queryString = split.length <= 1 ? '' : split[1]; + return new URLSearchParams(queryString); +} + +export function getTileRequest(tileRequest: TileRequest): { path?: string; body?: object } { + const searchParams = getSearchParams(tileRequest.tileUrl); + const encodedRequestBody = searchParams.has('requestBody') + ? (searchParams.get('requestBody') as string) + : '()'; + + if (!searchParams.has('index')) { + throw new Error(`Required query parameter 'index' not provided.`); + } + const index = searchParams.get('index') as string; + + if (!searchParams.has('geometryFieldName')) { + throw new Error(`Required query parameter 'geometryFieldName' not provided.`); + } + const geometryFieldName = searchParams.get('geometryFieldName') as string; + + if (tileRequest.tileUrl.includes(MVT_GETGRIDTILE_API_PATH)) { + return getAggsTileRequest({ + encodedRequestBody, + geometryFieldName, + gridPrecision: parseInt(searchParams.get('gridPrecision') as string, 10), + index, + renderAs: searchParams.get('renderAs') as RENDER_AS, + x: tileRequest.x, + y: tileRequest.y, + z: tileRequest.z, + }); + } + + if (tileRequest.tileUrl.includes(MVT_GETTILE_API_PATH)) { + return getHitsTileRequest({ + encodedRequestBody, + geometryFieldName, + index, + x: tileRequest.x, + y: tileRequest.y, + z: tileRequest.z, + }); + } + + throw new Error('Unexpected path'); +} diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/requests_view_callout.tsx b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/requests_view_callout.tsx new file mode 100644 index 0000000000000..0db29afaf7aef --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/requests_view_callout.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. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; + +export function RequestsViewCallout() { + return ( + + ); +} diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/tile_request_tab.tsx b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/tile_request_tab.tsx new file mode 100644 index 0000000000000..e57216a024b29 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/tile_request_tab.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + EuiButtonEmpty, + EuiCallOut, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { XJsonLang } from '@kbn/monaco'; +import { CodeEditor } from '@kbn/kibana-react-plugin/public'; +import { compressToEncodedURIComponent } from 'lz-string'; +import { + getDevToolsCapabilities, + getNavigateToUrl, + getShareService, +} from '../../../kibana_services'; +import type { TileRequest } from '../types'; +import { getTileRequest } from './get_tile_request'; + +interface Props { + tileRequest: TileRequest; +} + +export function TileRequestTab(props: Props) { + try { + const { path, body } = getTileRequest(props.tileRequest); + const consoleRequest = `POST ${path}\n${JSON.stringify(body, null, 2)}`; + let consoleHref: string | undefined; + if (getDevToolsCapabilities().show) { + const devToolsDataUri = compressToEncodedURIComponent(consoleRequest); + consoleHref = getShareService() + .url.locators.get('CONSOLE_APP_LOCATOR') + ?.useUrl({ loadFrom: `data:text/plain,${devToolsDataUri}` }); + } + return ( + + + + + +
    + + {(copy) => ( + + {i18n.translate( + 'xpack.maps.inspector.vectorTileRequest.copyToClipboardLabel', + { + defaultMessage: 'Copy to clipboard', + } + )} + + )} + +
    +
    + {consoleHref !== undefined && ( + +
    + { + const navigateToUrl = getNavigateToUrl(); + navigateToUrl(consoleHref!); + }} + iconType="wrench" + > + {i18n.translate('xpack.maps.inspector.vectorTileRequest.openInConsoleLabel', { + defaultMessage: 'Open in Console', + })} + +
    +
    + )} +
    +
    + + + +
    + ); + } catch (e) { + return ( + +

    + {i18n.translate('xpack.maps.inspector.vectorTileRequest.errorTitle', { + defaultMessage: `Could not convert tile request, '{tileUrl}', to Elasticesarch vector tile search request, error: {error}`, + values: { + tileUrl: props.tileRequest.tileUrl, + error: e.message, + }, + })} +

    +
    + ); + } +} diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/vector_tile_inspector.tsx b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/vector_tile_inspector.tsx new file mode 100644 index 0000000000000..9a9356ad1a6d2 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/components/vector_tile_inspector.tsx @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 _ from 'lodash'; +import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { Adapters } from '@kbn/inspector-plugin/public'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; +import { EmptyPrompt } from './empty_prompt'; +import type { TileRequest } from '../types'; +import { TileRequestTab } from './tile_request_tab'; +import { RequestsViewCallout } from './requests_view_callout'; + +interface Props { + adapters: Adapters; +} + +interface State { + selectedLayer: EuiComboBoxOptionOption | null; + selectedTileRequest: TileRequest | null; + tileRequests: TileRequest[]; + layerOptions: Array>; +} + +class VectorTileInspector extends Component { + private _isMounted = false; + + state: State = { + selectedLayer: null, + selectedTileRequest: null, + tileRequests: [], + layerOptions: [], + }; + + componentDidMount() { + this._isMounted = true; + this._onAdapterChange(); + this.props.adapters.vectorTiles.on('change', this._debouncedOnAdapterChange); + } + + componentWillUnmount() { + this._isMounted = false; + this.props.adapters.vectorTiles.removeListener('change', this._debouncedOnAdapterChange); + } + + _onAdapterChange = () => { + const layerOptions = this.props.adapters.vectorTiles.getLayerOptions() as Array< + EuiComboBoxOptionOption + >; + if (layerOptions.length === 0) { + this.setState({ + selectedLayer: null, + selectedTileRequest: null, + tileRequests: [], + layerOptions: [], + }); + return; + } + + const selectedLayer = + this.state.selectedLayer && + layerOptions.some((layerOption) => { + return this.state.selectedLayer?.value === layerOption.value; + }) + ? this.state.selectedLayer + : layerOptions[0]; + const tileRequests = this.props.adapters.vectorTiles.getTileRequests(selectedLayer.value); + const selectedTileRequest = + this.state.selectedTileRequest && + tileRequests.some((tileRequest: TileRequest) => { + return ( + this.state.selectedTileRequest?.layerId === tileRequest.layerId && + this.state.selectedTileRequest?.x === tileRequest.x && + this.state.selectedTileRequest?.y === tileRequest.y && + this.state.selectedTileRequest?.z === tileRequest.z + ); + }) + ? this.state.selectedTileRequest + : tileRequests.length + ? tileRequests[0] + : null; + + this.setState({ + selectedLayer, + selectedTileRequest, + tileRequests, + layerOptions, + }); + }; + + _debouncedOnAdapterChange = _.debounce(() => { + if (this._isMounted) { + this._onAdapterChange(); + } + }, 256); + + _onLayerSelect = (selectedOptions: Array>) => { + if (selectedOptions.length === 0) { + this.setState({ + selectedLayer: null, + selectedTileRequest: null, + tileRequests: [], + }); + return; + } + + const selectedLayer = selectedOptions[0]; + const tileRequests = this.props.adapters.vectorTiles.getTileRequests(selectedLayer.value); + this.setState({ + selectedLayer, + selectedTileRequest: tileRequests.length ? tileRequests[0] : null, + tileRequests, + }); + }; + + renderTabs() { + return this.state.tileRequests.map((tileRequest) => { + const tileLabel = `${tileRequest.z}/${tileRequest.x}/${tileRequest.y}`; + return ( + { + this.setState({ selectedTileRequest: tileRequest }); + }} + isSelected={ + tileRequest.layerId === this.state.selectedTileRequest?.layerId && + tileRequest.x === this.state.selectedTileRequest?.x && + tileRequest.y === this.state.selectedTileRequest?.y && + tileRequest.z === this.state.selectedTileRequest?.z + } + > + {tileLabel} + + ); + }); + } + + render() { + return this.state.layerOptions.length === 0 ? ( + <> + + + + ) : ( + <> + + + + + {this.renderTabs()} + + {this.state.selectedTileRequest && ( + + )} + + ); + } +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default VectorTileInspector; diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/index.ts b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/index.ts new file mode 100644 index 0000000000000..12dc5b318daba --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/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 { VectorTileAdapter } from './vector_tile_adapter'; +export { VectorTileInspectorView } from './vector_tile_inspector_view'; diff --git a/x-pack/plugins/maps/server/mvt/util.ts b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/types.ts similarity index 63% rename from x-pack/plugins/maps/server/mvt/util.ts rename to x-pack/plugins/maps/public/inspector/vector_tile_adapter/types.ts index d99bdfa0b7c52..5fead09165fad 100644 --- a/x-pack/plugins/maps/server/mvt/util.ts +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/types.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { errors } from '@elastic/elasticsearch'; - -export function isAbortError(error: Error) { - return error instanceof errors.RequestAbortedError; +export interface TileRequest { + layerId: string; + tileUrl: string; + x: number; + y: number; + z: number; } diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/vector_tile_adapter.ts b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/vector_tile_adapter.ts new file mode 100644 index 0000000000000..d0210367ab3b3 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/vector_tile_adapter.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EventEmitter } from 'events'; +import { TileRequest } from './types'; + +export class VectorTileAdapter extends EventEmitter { + private _layers: Record = {}; + private _tiles: Array<{ x: number; y: number; z: number }> = []; + + addLayer(layerId: string, label: string, tileUrl: string) { + this._layers[layerId] = { label, tileUrl }; + this._onChange(); + } + + removeLayer(layerId: string) { + delete this._layers[layerId]; + this._onChange(); + } + + setTiles(tiles: Array<{ x: number; y: number; z: number }>) { + this._tiles = tiles; + this._onChange(); + } + + getLayerOptions(): Array<{ value: string; label: string }> { + return Object.keys(this._layers).map((layerId) => { + return { + value: layerId, + label: this._layers[layerId].label, + }; + }); + } + + getTileRequests(layerId: string): TileRequest[] { + if (!this._layers[layerId]) { + return []; + } + + const { tileUrl } = this._layers[layerId]; + return this._tiles.map((tile) => { + return { + layerId, + tileUrl, + ...tile, + }; + }); + } + + _onChange() { + this.emit('change'); + } +} diff --git a/x-pack/plugins/maps/public/inspector/vector_tile_adapter/vector_tile_inspector_view.tsx b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/vector_tile_inspector_view.tsx new file mode 100644 index 0000000000000..42d7423e6e789 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/vector_tile_adapter/vector_tile_inspector_view.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy } from 'react'; +import type { Adapters } from '@kbn/inspector-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { LazyWrapper } from '../../lazy_wrapper'; + +const getLazyComponent = () => { + return lazy(() => import('./components/vector_tile_inspector')); +}; + +export const VectorTileInspectorView = { + title: i18n.translate('xpack.maps.inspector.vectorTileViewTitle', { + defaultMessage: 'Vector tiles', + }), + order: 10, + help: i18n.translate('xpack.maps.inspector.vectorTileViewHelpText', { + defaultMessage: 'View the vector tile search requests used to collect the data', + }), + shouldShow(adapters: Adapters) { + return Boolean(adapters.vectorTiles); + }, + component: (props: { adapters: Adapters }) => { + return ; + }, +}; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 22857c623c18a..5774a46684644 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -46,6 +46,7 @@ export const getTimeFilter = () => pluginsStart.data.query.timefilter.timefilter export const getToasts = () => coreStart.notifications.toasts; export const getSavedObjectsClient = () => coreStart.savedObjects.client; export const getCoreChrome = () => coreStart.chrome; +export const getDevToolsCapabilities = () => coreStart.application.capabilities.dev_tools; export const getMapsCapabilities = () => coreStart.application.capabilities.maps; export const getVisualizeCapabilities = () => coreStart.application.capabilities.visualize; export const getDocLinks = () => coreStart.docLinks; @@ -58,6 +59,7 @@ export const getCoreI18n = () => coreStart.i18n; export const getSearchService = () => pluginsStart.data.search; export const getEmbeddableService = () => pluginsStart.embeddable; export const getNavigateToApp = () => coreStart.application.navigateToApp; +export const getNavigateToUrl = () => coreStart.application.navigateToUrl; export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider; export const getSecurityService = () => pluginsStart.security; @@ -65,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 b5b232aeeaae6..846e7fc3d83f7 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -74,7 +74,7 @@ import { APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/cons import { getMapsVisTypeAlias } from './maps_vis_type_alias'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { setIsCloudEnabled, setMapAppConfig, setStartServices } from './kibana_services'; -import { MapInspectorView } from './inspector/map_inspector_view'; +import { MapInspectorView, VectorTileInspectorView } from './inspector'; import { setupLensChoroplethChart } from './lens'; @@ -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, }); @@ -172,6 +173,7 @@ export class MapsPlugin }) ); + plugins.inspector.registerView(VectorTileInspectorView); plugins.inspector.registerView(MapInspectorView); if (plugins.home) { plugins.home.featureCatalogue.register(featureCatalogueEntry); diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js index 7f43fe6f70398..fe1dba5505108 100644 --- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js +++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js @@ -6,7 +6,7 @@ */ import { RequestAdapter } from '@kbn/inspector-plugin/common/adapters/request'; -import { MapAdapter } from '../inspector/map_adapter'; +import { MapAdapter, VectorTileAdapter } from '../inspector'; import { getShowMapsInspectorAdapter } from '../kibana_services'; const REGISTER_CANCEL_CALLBACK = 'REGISTER_CANCEL_CALLBACK'; @@ -17,6 +17,7 @@ const SET_CHARTS_PALETTE_SERVICE_GET_COLOR = 'SET_CHARTS_PALETTE_SERVICE_GET_COL function createInspectorAdapters() { const inspectorAdapters = { requests: new RequestAdapter(), + vectorTiles: new VectorTileAdapter(), }; if (getShowMapsInspectorAdapter()) { inspectorAdapters.map = new MapAdapter(); diff --git a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts deleted file mode 100644 index eb0ddc9e13143..0000000000000 --- a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreStart, Logger } from '@kbn/core/server'; -import type { DataRequestHandlerContext } from '@kbn/data-plugin/server'; -import { IncomingHttpHeaders } from 'http'; -import { Stream } from 'stream'; -import { RENDER_AS } from '../../common/constants'; -import { isAbortError } from './util'; -import { makeExecutionContext } from '../../common/execution_context'; - -export async function getEsGridTile({ - url, - core, - logger, - context, - index, - geometryFieldName, - x, - y, - z, - requestBody = {}, - renderAs = RENDER_AS.POINT, - gridPrecision, - abortController, -}: { - url: string; - core: CoreStart; - x: number; - y: number; - z: number; - geometryFieldName: string; - index: string; - context: DataRequestHandlerContext; - logger: Logger; - requestBody: any; - renderAs: RENDER_AS; - gridPrecision: number; - abortController: AbortController; -}): Promise<{ stream: Stream | null; headers: IncomingHttpHeaders; statusCode: number }> { - try { - const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`; - const body = { - size: 0, // no hits - grid_precision: gridPrecision, - exact_bounds: false, - extent: 4096, // full resolution, - query: requestBody.query, - grid_agg: renderAs === RENDER_AS.HEX ? 'geohex' : 'geotile', - grid_type: renderAs === RENDER_AS.GRID || renderAs === RENDER_AS.HEX ? 'grid' : 'centroid', - aggs: requestBody.aggs, - fields: requestBody.fields, - runtime_mappings: requestBody.runtime_mappings, - }; - - const esClient = (await context.core).elasticsearch.client; - const tile = await core.executionContext.withContext( - makeExecutionContext({ - description: 'mvt:get_grid_tile', - url, - }), - async () => { - return await esClient.asCurrentUser.transport.request( - { - method: 'GET', - path, - body, - }, - { - signal: abortController.signal, - headers: { - 'Accept-Encoding': 'gzip', - }, - asStream: true, - meta: true, - } - ); - } - ); - - return { stream: tile.body as Stream, headers: tile.headers, statusCode: tile.statusCode }; - } catch (e) { - if (isAbortError(e)) { - return { stream: null, headers: {}, statusCode: 200 }; - } - - // These are often circuit breaking exceptions - // Should return a tile with some error message - logger.warn(`Cannot generate ES-grid-tile for ${z}/${x}/${y}: ${e.message}`); - return { stream: null, headers: {}, statusCode: 500 }; - } -} diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts deleted file mode 100644 index 340a71128b43a..0000000000000 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreStart, Logger } from '@kbn/core/server'; -import type { DataRequestHandlerContext } from '@kbn/data-plugin/server'; -import { IncomingHttpHeaders } from 'http'; -import { Stream } from 'stream'; -import { isAbortError } from './util'; -import { makeExecutionContext } from '../../common/execution_context'; -import { Field, mergeFields } from './merge_fields'; - -export async function getEsTile({ - url, - core, - logger, - context, - index, - geometryFieldName, - x, - y, - z, - requestBody = {}, - abortController, -}: { - url: string; - core: CoreStart; - x: number; - y: number; - z: number; - geometryFieldName: string; - index: string; - context: DataRequestHandlerContext; - logger: Logger; - requestBody: any; - abortController: AbortController; -}): Promise<{ stream: Stream | null; headers: IncomingHttpHeaders; statusCode: number }> { - try { - const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`; - - const body = { - grid_precision: 0, // no aggs - exact_bounds: true, - extent: 4096, // full resolution, - query: requestBody.query, - fields: mergeFields( - [ - requestBody.docvalue_fields as Field[] | undefined, - requestBody.stored_fields as Field[] | undefined, - ], - [geometryFieldName] - ), - runtime_mappings: requestBody.runtime_mappings, - track_total_hits: requestBody.size + 1, - }; - - const esClient = (await context.core).elasticsearch.client; - const tile = await core.executionContext.withContext( - makeExecutionContext({ - description: 'mvt:get_tile', - url, - }), - async () => { - return await esClient.asCurrentUser.transport.request( - { - method: 'GET', - path, - body, - }, - { - signal: abortController.signal, - headers: { - 'Accept-Encoding': 'gzip', - }, - asStream: true, - meta: true, - } - ); - } - ); - - return { stream: tile.body as Stream, headers: tile.headers, statusCode: tile.statusCode }; - } catch (e) { - if (isAbortError(e)) { - return { stream: null, headers: {}, statusCode: 200 }; - } - - // These are often circuit breaking exceptions - // Should return a tile with some error message - logger.warn(`Cannot generate ES-grid-tile for ${z}/${x}/${y}: ${e.message}`); - return { stream: null, headers: {}, statusCode: 500 }; - } -} diff --git a/x-pack/plugins/maps/server/mvt/merge_fields.ts b/x-pack/plugins/maps/server/mvt/merge_fields.ts deleted file mode 100644 index e371f3ff0715b..0000000000000 --- a/x-pack/plugins/maps/server/mvt/merge_fields.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// can not use "import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey" -// SearchRequest is incorrectly typed and does not support Field as object -// https://github.com/elastic/elasticsearch-js/issues/1615 -export type Field = - | string - | { - field: string; - format: string; - }; - -export function mergeFields( - fieldsList: Array, - excludeNames: string[] -): Field[] { - const fieldNames: string[] = []; - const mergedFields: Field[] = []; - - fieldsList.forEach((fields) => { - if (!fields) { - return; - } - - fields.forEach((field) => { - const fieldName = typeof field === 'string' ? field : field.field; - if (!excludeNames.includes(fieldName) && !fieldNames.includes(fieldName)) { - fieldNames.push(fieldName); - mergedFields.push(field); - } - }); - }); - - return mergedFields; -} diff --git a/x-pack/plugins/maps/server/mvt/mvt_routes.ts b/x-pack/plugins/maps/server/mvt/mvt_routes.ts index b7c6a59ba54d4..8af26548b1d28 100644 --- a/x-pack/plugins/maps/server/mvt/mvt_routes.ts +++ b/x-pack/plugins/maps/server/mvt/mvt_routes.ts @@ -8,18 +8,19 @@ import { Stream } from 'stream'; import { IncomingHttpHeaders } from 'http'; import { schema } from '@kbn/config-schema'; +import type { KibanaExecutionContext } from '@kbn/core/public'; import { CoreStart, KibanaRequest, KibanaResponseFactory, Logger } from '@kbn/core/server'; import { IRouter } from '@kbn/core/server'; import type { DataRequestHandlerContext } from '@kbn/data-plugin/server'; +import { errors } from '@elastic/elasticsearch'; import { MVT_GETTILE_API_PATH, API_ROOT_PATH, MVT_GETGRIDTILE_API_PATH, RENDER_AS, } from '../../common/constants'; -import { decodeMvtResponseBody } from '../../common/mvt_request_body'; -import { getEsTile } from './get_tile'; -import { getEsGridTile } from './get_grid_tile'; +import { makeExecutionContext } from '../../common/execution_context'; +import { getAggsTileRequest, getHitsTileRequest } from '../../common/mvt_request_body'; const CACHE_TIMEOUT_SECONDS = 60 * 60; @@ -55,21 +56,35 @@ export function initMVTRoutes({ response: KibanaResponseFactory ) => { const { query, params } = request; + const x = parseInt((params as any).x, 10) as number; + const y = parseInt((params as any).y, 10) as number; + const z = parseInt((params as any).z, 10) as number; - const abortController = makeAbortController(request); + let tileRequest: { path: string; body: object } | undefined; + try { + tileRequest = getHitsTileRequest({ + encodedRequestBody: query.requestBody as string, + geometryFieldName: query.geometryFieldName as string, + index: query.index as string, + x, + y, + z, + }); + } catch (e) { + return response.badRequest(); + } - const { stream, headers, statusCode } = await getEsTile({ - url: `${API_ROOT_PATH}/${MVT_GETTILE_API_PATH}/{z}/{x}/{y}.pbf`, + const { stream, headers, statusCode } = await getTile({ + abortController: makeAbortController(request), + body: tileRequest.body, + context, core, + executionContext: makeExecutionContext({ + description: 'mvt:get_hits_tile', + url: `${API_ROOT_PATH}/${MVT_GETTILE_API_PATH}/${z}/${x}/${y}.pbf`, + }), logger, - context, - geometryFieldName: query.geometryFieldName as string, - x: parseInt((params as any).x, 10) as number, - y: parseInt((params as any).y, 10) as number, - z: parseInt((params as any).z, 10) as number, - index: query.index as string, - requestBody: decodeMvtResponseBody(query.requestBody as string) as any, - abortController, + path: tileRequest.path, }); return sendResponse(response, stream, headers, statusCode); @@ -101,23 +116,37 @@ export function initMVTRoutes({ response: KibanaResponseFactory ) => { const { query, params } = request; + const x = parseInt((params as any).x, 10) as number; + const y = parseInt((params as any).y, 10) as number; + const z = parseInt((params as any).z, 10) as number; - const abortController = makeAbortController(request); + let tileRequest: { path: string; body: object } | undefined; + try { + tileRequest = getAggsTileRequest({ + encodedRequestBody: query.requestBody as string, + geometryFieldName: query.geometryFieldName as string, + gridPrecision: parseInt(query.gridPrecision, 10), + index: query.index as string, + renderAs: query.renderAs as RENDER_AS, + x, + y, + z, + }); + } catch (e) { + return response.badRequest(); + } - const { stream, headers, statusCode } = await getEsGridTile({ - url: `${API_ROOT_PATH}/${MVT_GETGRIDTILE_API_PATH}/{z}/{x}/{y}.pbf`, + const { stream, headers, statusCode } = await getTile({ + abortController: makeAbortController(request), + body: tileRequest.body, + context, core, + executionContext: makeExecutionContext({ + description: 'mvt:get_aggs_tile', + url: `${API_ROOT_PATH}/${MVT_GETGRIDTILE_API_PATH}/${z}/${x}/${y}.pbf`, + }), logger, - context, - geometryFieldName: query.geometryFieldName as string, - x: parseInt((params as any).x, 10) as number, - y: parseInt((params as any).y, 10) as number, - z: parseInt((params as any).z, 10) as number, - index: query.index as string, - requestBody: decodeMvtResponseBody(query.requestBody as string) as any, - renderAs: query.renderAs as RENDER_AS, - gridPrecision: parseInt(query.gridPrecision, 10), - abortController, + path: tileRequest.path, }); return sendResponse(response, stream, headers, statusCode); @@ -125,6 +154,56 @@ export function initMVTRoutes({ ); } +async function getTile({ + abortController, + body, + context, + core, + executionContext, + logger, + path, +}: { + abortController: AbortController; + body: object; + context: DataRequestHandlerContext; + core: CoreStart; + executionContext: KibanaExecutionContext; + logger: Logger; + path: string; +}) { + try { + const esClient = (await context.core).elasticsearch.client; + const tile = await core.executionContext.withContext(executionContext, async () => { + return await esClient.asCurrentUser.transport.request( + { + method: 'POST', + path, + body, + }, + { + signal: abortController.signal, + headers: { + 'Accept-Encoding': 'gzip', + }, + asStream: true, + meta: true, + } + ); + }); + + return { stream: tile.body as Stream, headers: tile.headers, statusCode: tile.statusCode }; + } catch (e) { + if (e instanceof errors.RequestAbortedError) { + return { stream: null, headers: {}, statusCode: 200 }; + } + + // These are often circuit breaking exceptions + // Should return a tile with some error message + logger.warn(`Cannot generate tile for ${executionContext.url}: ${e.message}`); + return { stream: null, headers: {}, statusCode: 500 }; + } +} + export function sendResponse( response: KibanaResponseFactory, tileStream: Stream | null, diff --git a/x-pack/plugins/ml/common/constants/job_actions.ts b/x-pack/plugins/ml/common/constants/job_actions.ts index 0cd16a10783e3..692875c73b105 100644 --- a/x-pack/plugins/ml/common/constants/job_actions.ts +++ b/x-pack/plugins/ml/common/constants/job_actions.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; - export const JOB_ACTION = { DELETE: 'delete', RESET: 'reset', @@ -15,22 +13,16 @@ export const JOB_ACTION = { export type JobAction = typeof JOB_ACTION[keyof typeof JOB_ACTION]; -export function getJobActionString(action: JobAction) { +export type JobActionState = 'deleting' | 'resetting' | 'reverting'; + +export function getJobActionString(action: JobAction): JobActionState { switch (action) { case JOB_ACTION.DELETE: - return i18n.translate('xpack.ml.models.jobService.deletingJob', { - defaultMessage: 'deleting', - }); + return 'deleting'; case JOB_ACTION.RESET: - return i18n.translate('xpack.ml.models.jobService.resettingJob', { - defaultMessage: 'resetting', - }); + return 'resetting'; case JOB_ACTION.REVERT: - return i18n.translate('xpack.ml.models.jobService.revertingJob', { - defaultMessage: 'reverting', - }); - default: - return ''; + return 'reverting'; } } 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/index.ts b/x-pack/plugins/ml/common/index.ts index de5989d92d208..cfed678a804a1 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -21,3 +21,4 @@ export { extractErrorMessage } from './util/errors'; export type { RuntimeMappings } from './types/fields'; export { getDefaultCapabilities as getDefaultMlCapabilities } from './types/capabilities'; export { DATAFEED_STATE, JOB_STATE } from './constants/states'; +export type { MlSummaryJob, SummaryJobState } from './types/anomaly_detection_jobs'; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index fed0cc85c20b0..504fdb8cf1dcd 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -5,15 +5,19 @@ * 2.0. */ +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Moment } from 'moment'; import { MlCustomSettings } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { CombinedJob, CombinedJobWithStats } from './combined_job'; import type { MlAnomalyDetectionAlertRule } from '../alerts'; import type { MlJobBlocked } from './job'; +import type { JobActionState } from '../../constants/job_actions'; export type { Datafeed } from './datafeed'; export type { DatafeedStats } from './datafeed_stats'; +export type SummaryJobState = estypes.MlJobState | JobActionState; + /** * A summary of an anomaly detection job. */ @@ -47,7 +51,7 @@ export interface MlSummaryJob { /** * The status of the job. */ - jobState: string; + jobState: SummaryJobState; /** * An array of index names used by the datafeed. Wildcards are supported. 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/readme.md b/x-pack/plugins/ml/readme.md index 9b155e5f7696c..a29e976f12c46 100644 --- a/x-pack/plugins/ml/readme.md +++ b/x-pack/plugins/ml/readme.md @@ -118,15 +118,15 @@ With PATH_TO_CONFIG and other options as follows. Group | PATH_TO_CONFIG ----- | -------------- anomaly detection | `test/functional/apps/ml/anomaly_detection/config.ts` - data frame analytics | `test/functional/apps/ml/anomaly_detection/config.ts` - data visualizer | `test/functional/apps/ml/data_frame_analytics/config.ts` + data frame analytics | `test/functional/apps/ml/data_frame_analytics/config.ts` + data visualizer | `test/functional/apps/ml/data_visualizer/config.ts` permissions | `test/functional/apps/ml/permissions/config.ts` stack management jobs | `test/functional/apps/ml/stack_management_jobs/config.ts` short tests | `test/functional/apps/ml/short_tests/config.ts` The `short tests` group contains tests for page navigation, model management, feature controls, settings and embeddables. Test files for each group are located - in the directory of their copnfiguration file. + in the directory of their configuration file. 1. Functional UI tests with `Basic` license: diff --git a/x-pack/plugins/ml/server/index.ts b/x-pack/plugins/ml/server/index.ts index 786920ef5e46e..8a1cfb9590402 100644 --- a/x-pack/plugins/ml/server/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -14,6 +14,8 @@ export type { AnomalyResultType as MlAnomalyResultType, DatafeedStats as MlDatafeedStats, Job as MlJob, + MlSummaryJob, + SummaryJobState as MlSummaryJobState, } from './shared'; export { UnknownMLCapabilitiesError, 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_last24h_alerts.ts b/x-pack/plugins/observability/public/hooks/use_fetch_last24h_alerts.ts new file mode 100644 index 0000000000000..cc1313be29340 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetch_last24h_alerts.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState, useCallback, useRef } from 'react'; +import { AsApiContract } from '@kbn/actions-plugin/common'; +import { HttpSetup } from '@kbn/core/public'; +import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants'; +import { RULE_LOAD_ERROR } from '../pages/rule_details/translations'; + +interface UseFetchLast24hAlertsProps { + http: HttpSetup; + features: string; + ruleId: string; +} +interface FetchLast24hAlerts { + isLoadingLast24hAlerts: boolean; + last24hAlerts: number; + errorLast24hAlerts: string | undefined; +} + +export function useFetchLast24hAlerts({ http, features, ruleId }: UseFetchLast24hAlertsProps) { + const [last24hAlerts, setLast24hAlerts] = useState({ + isLoadingLast24hAlerts: true, + last24hAlerts: 0, + errorLast24hAlerts: undefined, + }); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + const fetchLast24hAlerts = useCallback(async () => { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + try { + if (!features) return; + const { index } = await fetchIndexNameAPI({ + http, + features, + }); + const { error, alertsCount } = await fetchLast24hAlertsAPI({ + http, + index, + ruleId, + signal: abortCtrlRef.current.signal, + }); + if (error) throw error; + if (!isCancelledRef.current) { + setLast24hAlerts((oldState: FetchLast24hAlerts) => ({ + ...oldState, + last24hAlerts: alertsCount, + isLoading: false, + })); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + setLast24hAlerts((oldState: FetchLast24hAlerts) => ({ + ...oldState, + isLoading: false, + errorLast24hAlerts: RULE_LOAD_ERROR( + error instanceof Error ? error.message : typeof error === 'string' ? error : '' + ), + })); + } + } + } + }, [http, features, ruleId]); + useEffect(() => { + fetchLast24hAlerts(); + }, [fetchLast24hAlerts]); + + return last24hAlerts; +} + +interface IndexName { + index: string; +} + +export async function fetchIndexNameAPI({ + http, + features, +}: { + http: HttpSetup; + features: string; +}): Promise { + const res = await http.get<{ index_name: string[] }>(`${BASE_RAC_ALERTS_API_PATH}/index`, { + query: { features }, + }); + return { + index: res.index_name[0], + }; +} +export async function fetchLast24hAlertsAPI({ + http, + index, + ruleId, + signal, +}: { + http: HttpSetup; + index: string; + ruleId: string; + signal: AbortSignal; +}): Promise<{ + error: string | null; + alertsCount: number; +}> { + try { + const res = await http.post>(`${BASE_RAC_ALERTS_API_PATH}/find`, { + signal, + body: JSON.stringify({ + index, + query: { + bool: { + must: [ + { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + { + range: { + '@timestamp': { + gte: 'now-24h', + lt: 'now', + }, + }, + }, + ], + }, + }, + aggs: { + alerts_count: { + cardinality: { + field: 'kibana.alert.uuid', + }, + }, + }, + }), + }); + return { + error: null, + alertsCount: res.aggregations.alerts_count.value, + }; + } catch (error) { + return { + error, + alertsCount: 0, + }; + } +} diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts new file mode 100644 index 0000000000000..07f13b4c80e7e --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState, useCallback } from 'react'; +import { loadRule } from '@kbn/triggers-actions-ui-plugin/public'; +import { FetchRuleProps, FetchRule } from '../pages/rule_details/types'; +import { RULE_LOAD_ERROR } from '../pages/rule_details/translations'; + +export function useFetchRule({ ruleId, http }: FetchRuleProps) { + const [ruleSummary, setRuleSummary] = useState({ + isRuleLoading: true, + rule: undefined, + errorRule: undefined, + }); + const fetchRuleSummary = useCallback(async () => { + try { + const rule = await loadRule({ + http, + ruleId, + }); + + setRuleSummary((oldState: FetchRule) => ({ + ...oldState, + isRuleLoading: false, + rule, + })); + } catch (error) { + setRuleSummary((oldState: FetchRule) => ({ + ...oldState, + isRuleLoading: false, + errorRule: RULE_LOAD_ERROR( + error instanceof Error ? error.message : typeof error === 'string' ? error : '' + ), + })); + } + }, [ruleId, http]); + useEffect(() => { + fetchRuleSummary(); + }, [fetchRuleSummary]); + + return { ...ruleSummary, reloadRule: fetchRuleSummary }; +} diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rule_actions.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rule_actions.ts new file mode 100644 index 0000000000000..eaf01ed5ba59d --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rule_actions.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState, useCallback } from 'react'; +import { ActionConnector, loadAllActions } from '@kbn/triggers-actions-ui-plugin/public'; +import { FetchRuleActionsProps } from '../pages/rule_details/types'; +import { ACTIONS_LOAD_ERROR } from '../pages/rule_details/translations'; + +interface FetchActions { + isLoadingActions: boolean; + allActions: Array>>; + errorActions: string | undefined; +} + +export function useFetchRuleActions({ http }: FetchRuleActionsProps) { + const [ruleActions, setRuleActions] = useState({ + isLoadingActions: true, + allActions: [] as Array>>, + errorActions: undefined, + }); + + const fetchRuleActions = useCallback(async () => { + try { + const response = await loadAllActions({ + http, + }); + setRuleActions((oldState: FetchActions) => ({ + ...oldState, + isLoadingActions: false, + allActions: response, + })); + } catch (error) { + setRuleActions((oldState: FetchActions) => ({ + ...oldState, + isLoadingActions: false, + errorActions: ACTIONS_LOAD_ERROR( + error instanceof Error ? error.message : typeof error === 'string' ? error : '' + ), + })); + } + }, [http]); + useEffect(() => { + fetchRuleActions(); + }, [fetchRuleActions]); + + return { ...ruleActions, reloadRuleActions: fetchRuleActions }; +} diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rule_summary.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rule_summary.ts new file mode 100644 index 0000000000000..7e7c71e503329 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rule_summary.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState, useCallback } from 'react'; +import { loadRuleSummary } from '@kbn/triggers-actions-ui-plugin/public'; +import { FetchRuleSummaryProps, FetchRuleSummary } from '../pages/rule_details/types'; +import { RULE_LOAD_ERROR } from '../pages/rule_details/translations'; + +export function useFetchRuleSummary({ ruleId, http }: FetchRuleSummaryProps) { + const [ruleSummary, setRuleSummary] = useState({ + isLoadingRuleSummary: true, + ruleSummary: undefined, + errorRuleSummary: undefined, + }); + + const fetchRuleSummary = useCallback(async () => { + setRuleSummary((oldState: FetchRuleSummary) => ({ ...oldState, isLoading: true })); + + try { + const response = await loadRuleSummary({ + http, + ruleId, + }); + setRuleSummary((oldState: FetchRuleSummary) => ({ + ...oldState, + isLoading: false, + ruleSummary: response, + })); + } catch (error) { + setRuleSummary((oldState: FetchRuleSummary) => ({ + ...oldState, + isLoading: false, + errorRuleSummary: RULE_LOAD_ERROR( + error instanceof Error ? error.message : typeof error === 'string' ? error : '' + ), + })); + } + }, [ruleId, http]); + useEffect(() => { + fetchRuleSummary(); + }, [fetchRuleSummary]); + + return { ...ruleSummary, reloadRuleSummary: fetchRuleSummary }; +} 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/rule_details/components/actions.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx new file mode 100644 index 0000000000000..e3aadb60f8c4c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/actions.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { + EuiText, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + IconType, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { intersectionBy } from 'lodash'; +import { ActionsProps } from '../types'; +import { useFetchRuleActions } from '../../../hooks/use_fetch_rule_actions'; +import { useKibana } from '../../../utils/kibana_react'; + +interface MapActionTypeIcon { + [key: string]: string | IconType; +} +const mapActionTypeIcon: MapActionTypeIcon = { + /* TODO: Add the rest of the application logs (SVGs ones) */ + '.server-log': 'logsApp', + '.email': 'email', + '.pagerduty': 'apps', + '.index': 'indexOpen', + '.slack': 'logoSlack', + '.webhook': 'logoWebhook', +}; +export function Actions({ ruleActions }: ActionsProps) { + const { + http, + notifications: { toasts }, + } = useKibana().services; + const { isLoadingActions, allActions, errorActions } = useFetchRuleActions({ http }); + if (ruleActions && ruleActions.length <= 0) return 0; + const actions = intersectionBy(allActions, ruleActions, 'actionTypeId'); + if (isLoadingActions) return ; + return ( + + {actions.map((action) => ( + <> + + + + + + {action.name} + + + + + ))} + {errorActions && toasts.addDanger({ title: errorActions })} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/index.ts b/x-pack/plugins/observability/public/pages/rule_details/components/index.ts new file mode 100644 index 0000000000000..8020af09dedc2 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PageTitle } from './page_title'; +export { ItemTitleRuleSummary } from './item_title_rule_summary'; +export { ItemValueRuleSummary } from './item_value_rule_summary'; +export { Actions } from './actions'; diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/item_title_rule_summary.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/item_title_rule_summary.tsx new file mode 100644 index 0000000000000..d2a4805938305 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/item_title_rule_summary.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { ItemTitleRuleSummaryProps } from '../types'; + +export function ItemTitleRuleSummary({ children }: ItemTitleRuleSummaryProps) { + return ( + + + {children} + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/item_value_rule_summary.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/item_value_rule_summary.tsx new file mode 100644 index 0000000000000..6e178250c53ff --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/item_value_rule_summary.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlexItem, EuiText } from '@elastic/eui'; +import { ItemValueRuleSummaryProps } from '../types'; + +export function ItemValueRuleSummary({ itemValue, extraSpace = true }: ItemValueRuleSummaryProps) { + return ( + + {itemValue} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx new file mode 100644 index 0000000000000..478fbf69a226c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; +import moment from 'moment'; +import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ExperimentalBadge } from '../../../components/shared/experimental_badge'; +import { PageHeaderProps } from '../types'; +import { useKibana } from '../../../utils/kibana_react'; +import { LAST_UPDATED_MESSAGE, CREATED_WORD, BY_WORD, ON_WORD } from '../translations'; + +export function PageTitle({ rule }: PageHeaderProps) { + const { triggersActionsUi } = useKibana().services; + const [isTagsPopoverOpen, setIsTagsPopoverOpen] = useState(false); + const tagsClicked = () => + setIsTagsPopoverOpen( + (oldStateIsTagsPopoverOpen) => rule.tags.length > 0 && !oldStateIsTagsPopoverOpen + ); + const closeTagsPopover = () => setIsTagsPopoverOpen(false); + return ( + <> + {rule.name} + + + + {LAST_UPDATED_MESSAGE} {BY_WORD} {rule.updatedBy} {ON_WORD}  + {moment(rule.updatedAt).format('ll')}   + {CREATED_WORD} {BY_WORD} {rule.createdBy} {ON_WORD}  + {moment(rule.createdAt).format('ll')} + + + + {rule.tags.length > 0 && + triggersActionsUi.getRuleTagBadge({ + isOpen: isTagsPopoverOpen, + tags: rule.tags, + onClick: () => tagsClicked(), + onClose: () => closeTagsPopover(), + })} + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/config.ts b/x-pack/plugins/observability/public/pages/rule_details/config.ts new file mode 100644 index 0000000000000..e73849f47e7b3 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/config.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleType, Rule } from '@kbn/triggers-actions-ui-plugin/public'; + +type Capabilities = Record; + +export type InitialRule = Partial & + Pick; + +export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { + return ruleType?.authorizedConsumers[rule.consumer]?.all ?? false; +} + +export const hasExecuteActionsCapability = (capabilities: Capabilities) => + capabilities?.actions?.execute; + +export const RULES_PAGE_LINK = '/app/observability/alerts/rules'; +export const ALERT_PAGE_LINK = '/app/observability/alerts'; diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx new file mode 100644 index 0000000000000..ce7049bd61056 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -0,0 +1,483 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import moment from 'moment'; +import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { + EuiText, + EuiSpacer, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiPanel, + EuiTitle, + EuiHealth, + EuiPopover, + EuiHorizontalRule, + EuiTabbedContent, + EuiEmptyPrompt, +} from '@elastic/eui'; + +import { + enableRule, + disableRule, + snoozeRule, + unsnoozeRule, + deleteRules, + useLoadRuleTypes, + RuleType, +} from '@kbn/triggers-actions-ui-plugin/public'; +// TODO: use a Delete modal from triggersActionUI when it's sharable +import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; +import { DeleteModalConfirmation } from '../rules/components/delete_modal_confirmation'; +import { CenterJustifiedSpinner } from '../rules/components/center_justified_spinner'; +import { getHealthColor, OBSERVABILITY_SOLUTIONS } from '../rules/config'; +import { + RuleDetailsPathParams, + EVENT_ERROR_LOG_TAB, + EVENT_LOG_LIST_TAB, + ALERT_LIST_TAB, +} from './types'; +import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useFetchRule } from '../../hooks/use_fetch_rule'; +import { RULES_BREADCRUMB_TEXT } from '../rules/translations'; +import { PageTitle, ItemTitleRuleSummary, ItemValueRuleSummary, Actions } from './components'; +import { useKibana } from '../../utils/kibana_react'; +import { useFetchLast24hAlerts } from '../../hooks/use_fetch_last24h_alerts'; +import { formatInterval } from './utils'; +import { + hasExecuteActionsCapability, + hasAllPrivilege, + RULES_PAGE_LINK, + ALERT_PAGE_LINK, +} from './config'; + +export function RuleDetailsPage() { + const { + http, + triggersActionsUi: { ruleTypeRegistry, getRuleStatusDropdown, getEditAlertFlyout }, + application: { capabilities, navigateToUrl }, + notifications: { toasts }, + } = useKibana().services; + + const { ruleId } = useParams(); + const { ObservabilityPageTemplate } = usePluginContext(); + const { isRuleLoading, rule, errorRule, reloadRule } = useFetchRule({ ruleId, http }); + const { ruleTypes } = useLoadRuleTypes({ + filteredSolutions: OBSERVABILITY_SOLUTIONS, + }); + + const [features, setFeatures] = useState(''); + const [ruleType, setRuleType] = useState>(); + const [ruleToDelete, setRuleToDelete] = useState([]); + const [isPageLoading, setIsPageLoading] = useState(false); + const { last24hAlerts } = useFetchLast24hAlerts({ + http, + features, + ruleId, + }); + + const [editFlyoutVisible, setEditFlyoutVisible] = useState(false); + const [isRuleEditPopoverOpen, setIsRuleEditPopoverOpen] = useState(false); + + const handleClosePopover = useCallback(() => setIsRuleEditPopoverOpen(false), []); + + const handleOpenPopover = useCallback(() => setIsRuleEditPopoverOpen(true), []); + + const handleRemoveRule = useCallback(() => { + setIsRuleEditPopoverOpen(false); + if (rule) setRuleToDelete([rule.id]); + }, [rule]); + + const handleEditRule = useCallback(() => { + setIsRuleEditPopoverOpen(false); + setEditFlyoutVisible(true); + }, []); + + useEffect(() => { + if (ruleTypes.length && rule) { + const matchedRuleType = ruleTypes.find((type) => type.id === rule.ruleTypeId); + if (rule.consumer === ALERTS_FEATURE_ID && matchedRuleType && matchedRuleType.producer) { + setRuleType(matchedRuleType); + setFeatures(matchedRuleType.producer); + } else setFeatures(rule.consumer); + } + }, [rule, ruleTypes]); + + useBreadcrumbs([ + { + text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { + defaultMessage: 'Alerts', + }), + href: http.basePath.prepend(ALERT_PAGE_LINK), + }, + { + href: http.basePath.prepend(RULES_PAGE_LINK), + text: RULES_BREADCRUMB_TEXT, + }, + { + text: rule && rule.name, + }, + ]); + + const canExecuteActions = hasExecuteActionsCapability(capabilities); + const canSaveRule = + rule && + hasAllPrivilege(rule, ruleType) && + // if the rule has actions, can the user save the rule's action params + (canExecuteActions || (!canExecuteActions && rule.actions.length === 0)); + + const hasEditButton = + // can the user save the rule + canSaveRule && + // is this rule type editable from within Rules Management + (ruleTypeRegistry.has(rule.ruleTypeId) + ? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext + : false); + + const getRuleConditionsWording = () => { + const numberOfConditions = rule?.params.criteria ? (rule?.params.criteria as any[]).length : 0; + return ( + <> + {numberOfConditions}  + {i18n.translate('xpack.observability.ruleDetails.conditions', { + defaultMessage: 'condition{s}', + values: { s: numberOfConditions > 1 ? 's' : '' }, + })} + + ); + }; + + const tabs = [ + { + id: EVENT_LOG_LIST_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.eventLogTabText', { + defaultMessage: 'Execution history', + }), + 'data-test-subj': 'eventLogListTab', + content: Execution history, + }, + { + id: ALERT_LIST_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.alertsTabText', { + defaultMessage: 'Alerts', + }), + 'data-test-subj': 'ruleAlertListTab', + content: Alerts, + }, + { + id: EVENT_ERROR_LOG_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.errorLogTabText', { + defaultMessage: 'Error log', + }), + 'data-test-subj': 'errorLogTab', + content: Error log, + }, + ]; + + if (isPageLoading || isRuleLoading) return ; + if (!rule || errorRule) + return ( + + + {i18n.translate('xpack.observability.ruleDetails.errorPromptTitle', { + defaultMessage: 'Unable to load rule details', + })} + + } + body={ +

    + {i18n.translate('xpack.observability.ruleDetails.errorPromptBody', { + defaultMessage: 'There was an error loading the rule details.', + })} +

    + } + /> +
    + ); + return ( + , + bottomBorder: false, + rightSideItems: hasEditButton + ? [ + + + + } + > + + + + + {i18n.translate('xpack.observability.ruleDetails.editRule', { + defaultMessage: 'Edit rule', + })} + + + + + + {i18n.translate('xpack.observability.ruleDetails.deleteRule', { + defaultMessage: 'Delete rule', + })} + + + + + + + + + + + {i18n.translate('xpack.observability.ruleDetails.triggreAction.status', { + defaultMessage: 'Status', + })} + + + + {getRuleStatusDropdown({ + rule, + enableRule: async () => await enableRule({ http, id: rule.id }), + disableRule: async () => await disableRule({ http, id: rule.id }), + onRuleChanged: () => reloadRule(), + isEditable: hasEditButton, + snoozeRule: async (snoozeEndTime: string | -1) => { + await snoozeRule({ http, id: rule.id, snoozeEndTime }); + }, + unsnoozeRule: async () => await unsnoozeRule({ http, id: rule.id }), + })} + + , + ] + : [], + }} + > + + {/* Left side of Rule Summary */} + + + + + + + {rule.executionStatus.status.charAt(0).toUpperCase() + + rule.executionStatus.status.slice(1)} + + + + + + + {i18n.translate('xpack.observability.ruleDetails.lastRun', { + defaultMessage: 'Last Run', + })} + + + + + + + + + + + {i18n.translate('xpack.observability.ruleDetails.alerts', { + defaultMessage: 'Alerts', + })} + + + + + + + + + + + {/* Right side of Rule Summary */} + + + + + + + {i18n.translate('xpack.observability.ruleDetails.definition', { + defaultMessage: 'Definition', + })} + + + {hasEditButton && ( + + setEditFlyoutVisible(true)} /> + + )} + + + + + + + + + {i18n.translate('xpack.observability.ruleDetails.ruleType', { + defaultMessage: 'Rule type', + })} + + + + + + + + + {i18n.translate('xpack.observability.ruleDetails.description', { + defaultMessage: 'Description', + })} + + + + + + + + + {i18n.translate('xpack.observability.ruleDetails.conditionsTitle', { + defaultMessage: 'Conditions', + })} + + + + {hasEditButton ? ( + setEditFlyoutVisible(true)}> + {getRuleConditionsWording()} + + ) : ( + {getRuleConditionsWording()} + )} + + + + + + + + + + {i18n.translate('xpack.observability.ruleDetails.runsEvery', { + defaultMessage: 'Runs every', + })} + + + + + + + + + + {i18n.translate('xpack.observability.ruleDetails.notifyWhen', { + defaultMessage: 'Notify', + })} + + + + + + + + + {i18n.translate('xpack.observability.ruleDetails.actions', { + defaultMessage: 'Actions', + })} + + + + + + + + + + + + + + {editFlyoutVisible && + getEditAlertFlyout({ + initialRule: rule, + onClose: () => { + setEditFlyoutVisible(false); + }, + onSave: reloadRule, + })} + { + setRuleToDelete([]); + navigateToUrl(http.basePath.prepend(RULES_PAGE_LINK)); + }} + onErrors={async () => { + setRuleToDelete([]); + navigateToUrl(http.basePath.prepend(RULES_PAGE_LINK)); + }} + onCancel={() => {}} + apiDeleteCall={deleteRules} + idsToDelete={ruleToDelete} + singleTitle={rule.name} + multipleTitle={rule.name} + setIsLoadingState={(isLoading: boolean) => { + setIsPageLoading(isLoading); + }} + /> + {errorRule && toasts.addDanger({ title: errorRule })} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/translations.ts b/x-pack/plugins/observability/public/pages/rule_details/translations.ts new file mode 100644 index 0000000000000..f162f30906c21 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/translations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +export const RULE_LOAD_ERROR = (errorMessage: string) => + i18n.translate('xpack.observability.ruleDetails.ruleLoadError', { + defaultMessage: 'Unable to load rule. Reason: {message}', + values: { message: errorMessage }, + }); + +export const ACTIONS_LOAD_ERROR = (errorMessage: string) => + i18n.translate('xpack.observability.ruleDetails.connectorsLoadError', { + defaultMessage: 'Unable to load rule actions connectors. Reason: {message}', + values: { message: errorMessage }, + }); + +export const TAGS_TITLE = i18n.translate('xpack.observability.ruleDetails.tagsTitle', { + defaultMessage: 'Tags', +}); + +export const LAST_UPDATED_MESSAGE = i18n.translate( + 'xpack.observability.ruleDetails.lastUpdatedMessage', + { + defaultMessage: 'Last updated', + } +); + +export const BY_WORD = i18n.translate('xpack.observability.ruleDetails.byWord', { + defaultMessage: 'by', +}); + +export const ON_WORD = i18n.translate('xpack.observability.ruleDetails.onWord', { + defaultMessage: 'on', +}); + +export const CREATED_WORD = i18n.translate('xpack.observability.ruleDetails.createdWord', { + defaultMessage: 'Created', +}); diff --git a/x-pack/plugins/observability/public/pages/rule_details/types.ts b/x-pack/plugins/observability/public/pages/rule_details/types.ts new file mode 100644 index 0000000000000..9855bf2c7f184 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/types.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from '@kbn/core/public'; +import { Rule, RuleSummary, RuleType } from '@kbn/triggers-actions-ui-plugin/public'; + +export interface RuleDetailsPathParams { + ruleId: string; +} +export interface PageHeaderProps { + rule: Rule; +} + +export interface FetchRuleProps { + ruleId: string; + http: HttpSetup; +} + +export interface FetchRule { + isRuleLoading: boolean; + rule?: Rule; + ruleType?: RuleType; + errorRule?: string; +} + +export interface FetchRuleSummaryProps { + ruleId: string; + http: HttpSetup; +} +export interface FetchRuleActionsProps { + http: HttpSetup; +} + +export interface FetchRuleSummary { + isLoadingRuleSummary: boolean; + ruleSummary?: RuleSummary; + errorRuleSummary?: string; +} + +export interface AlertListItemStatus { + label: string; + healthColor: string; + actionGroup?: string; +} +export interface AlertListItem { + alert: string; + status: AlertListItemStatus; + start?: Date; + duration: number; + isMuted: boolean; + sortPriority: number; +} +export interface ItemTitleRuleSummaryProps { + children: string; +} +export interface ItemValueRuleSummaryProps { + itemValue: string; + extraSpace?: boolean; +} +export interface ActionsProps { + ruleActions: any[]; +} + +export const EVENT_LOG_LIST_TAB = 'rule_event_log_list'; +export const ALERT_LIST_TAB = 'rule_alert_list'; +export const EVENT_ERROR_LOG_TAB = 'rule_error_log_list'; diff --git a/x-pack/plugins/observability/public/pages/rule_details/utils.ts b/x-pack/plugins/observability/public/pages/rule_details/utils.ts new file mode 100644 index 0000000000000..0c907d93228a6 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/utils.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 { formatDurationFromTimeUnitChar, TimeUnitChar } from '../../../common'; + +export const formatInterval = (ruleInterval: string) => { + const interval: string[] | null = ruleInterval.match(/(^\d*)([s|m|h|d])/); + if (!interval || interval.length < 3) return ruleInterval; + const value: number = +interval[1]; + const unit = interval[2] as TimeUnitChar; + return formatDurationFromTimeUnitChar(value, unit); +}; diff --git a/x-pack/plugins/observability/public/pages/rules/components/name.tsx b/x-pack/plugins/observability/public/pages/rules/components/name.tsx index cbde68ea27eb4..15cb44412d880 100644 --- a/x-pack/plugins/observability/public/pages/rules/components/name.tsx +++ b/x-pack/plugins/observability/public/pages/rules/components/name.tsx @@ -12,9 +12,7 @@ import { useKibana } from '../../../utils/kibana_react'; export function Name({ name, rule }: RuleNameProps) { const { http } = useKibana().services; - const detailsLink = http.basePath.prepend( - `/app/management/insightsAndAlerting/triggersActions/rule/${rule.id}` - ); + const detailsLink = http.basePath.prepend(`/app/observability/alerts/rules/${rule.id}`); const link = ( 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..867e44613e07c 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,8 @@ 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 { RuleDetailsPage } from '../pages/rule_details'; +import { AlertingPages } from '../config'; export type RouteParams = DecodeParams; @@ -60,14 +63,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 +101,18 @@ export const routes = { }, '/alerts/rules': { handler: () => { - return ; + return ( + + + + ); + }, + params: {}, + exact: true, + }, + '/alerts/rules/:ruleId': { + handler: () => { + return ; }, params: {}, exact: true, diff --git a/x-pack/plugins/osquery/server/lib/telemetry/sender.ts b/x-pack/plugins/osquery/server/lib/telemetry/sender.ts index ab5af7d60f466..a2acc2fe8ec6f 100644 --- a/x-pack/plugins/osquery/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/osquery/server/lib/telemetry/sender.ts @@ -267,8 +267,8 @@ export class TelemetryEventsSender { const resp = await axios.post(telemetryUrl, ndjson, { headers: { 'Content-Type': 'application/x-ndjson', - 'X-Elastic-Cluster-ID': clusterUuid, - 'X-Elastic-Cluster-Name': clusterName, + ...(clusterUuid ? { 'X-Elastic-Cluster-ID': clusterUuid } : undefined), + ...(clusterName ? { 'X-Elastic-Cluster-Name': clusterName } : undefined), 'X-Elastic-Stack-Version': clusterVersionNumber ? clusterVersionNumber : '8.0.0', ...(licenseId ? { 'X-Elastic-License-ID': licenseId } : {}), }, 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 + + + + +
    +
    +
    -
    - -